Refactol llm pkg
This commit is contained in:
@@ -72,9 +72,13 @@ func (c *Client) Completions(
|
||||
func (c *Client) completions(
|
||||
ctx context.Context, p CompletionsParams,
|
||||
) (*ChatResponse, error) {
|
||||
kwargs := map[string]any{}
|
||||
// A nil map is omitted from the request; an empty map is
|
||||
// sent as {}. Keep kwargs nil without a budget.
|
||||
var kwargs map[string]any
|
||||
if p.ThinkingBudgetTokens != nil {
|
||||
kwargs[chatTemplateEnableThinking] = *p.ThinkingBudgetTokens != 0
|
||||
kwargs = map[string]any{
|
||||
chatTemplateEnableThinking: *p.ThinkingBudgetTokens != 0,
|
||||
}
|
||||
}
|
||||
body, err := json.Marshal(ChatRequest{
|
||||
Model: p.Model,
|
||||
@@ -270,9 +274,13 @@ func (c *Client) CompletionsStream(
|
||||
) (*CompletionStream, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
|
||||
kwargs := map[string]any{}
|
||||
// A nil map is omitted from the request; an empty map is
|
||||
// sent as {}. Keep kwargs nil without a budget.
|
||||
var kwargs map[string]any
|
||||
if p.ThinkingBudgetTokens != nil {
|
||||
kwargs[chatTemplateEnableThinking] = *p.ThinkingBudgetTokens != 0
|
||||
kwargs = map[string]any{
|
||||
chatTemplateEnableThinking: *p.ThinkingBudgetTokens != 0,
|
||||
}
|
||||
}
|
||||
body, err := json.Marshal(ChatRequest{
|
||||
ChatTemplateKWARGS: kwargs,
|
||||
|
||||
+117
-75
@@ -39,7 +39,7 @@ func (e *APIError) Error() string {
|
||||
// Type is ToolTypeFunction.
|
||||
type APITool struct {
|
||||
// Function defines the callable function when Type is ToolTypeFunction.
|
||||
Function *FunctionDef `json:"function,omitempty"`
|
||||
Function *FunctionDef `json:"function,omitzero"`
|
||||
// Type selects the representation of the tool.
|
||||
Type ToolType `json:"type"`
|
||||
}
|
||||
@@ -53,65 +53,65 @@ type ChatRequest struct {
|
||||
// ChatTemplateKWARGS contains arguments made available to the
|
||||
// endpoint's chat template.
|
||||
// It is a llama.cpp extension to the OpenAI API.
|
||||
ChatTemplateKWARGS map[string]any `json:"chat_template_kwargs,omitempty"`
|
||||
ChatTemplateKWARGS map[string]any `json:"chat_template_kwargs,omitzero"`
|
||||
// FrequencyPenalty adjusts token likelihood according to frequency in
|
||||
// the generated text. Nil leaves the choice to the endpoint.
|
||||
FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"`
|
||||
FrequencyPenalty *float32 `json:"frequency_penalty,omitzero"`
|
||||
// LogProbs requests log probabilities for generated tokens.
|
||||
LogProbs bool `json:"logprobs,omitempty"`
|
||||
LogProbs bool `json:"logprobs,omitzero"`
|
||||
// MaxCompletionTokens bounds generated tokens, including reasoning
|
||||
// tokens.
|
||||
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
|
||||
MaxCompletionTokens int `json:"max_completion_tokens,omitzero"`
|
||||
// MaxTokens is the deprecated predecessor of MaxCompletionTokens.
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitzero"`
|
||||
// Messages is the conversation presented to the model, in order.
|
||||
Messages []Message `json:"messages"`
|
||||
// Model identifies the model to use according to the endpoint.
|
||||
Model string `json:"model"`
|
||||
// N is the number of alternatives to generate.
|
||||
N int `json:"n,omitempty"`
|
||||
N int `json:"n,omitzero"`
|
||||
// ParallelToolCalls controls whether the model may request multiple
|
||||
// function calls in one turn. Nil leaves the choice to the endpoint.
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitzero"`
|
||||
// PresencePenalty adjusts token likelihood according to whether a
|
||||
// token has already appeared. Nil leaves the choice to the endpoint.
|
||||
PresencePenalty *float32 `json:"presence_penalty,omitempty"`
|
||||
PresencePenalty *float32 `json:"presence_penalty,omitzero"`
|
||||
// ResponseFormat constrains the representation of generated content.
|
||||
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
|
||||
ResponseFormat *ResponseFormat `json:"response_format,omitzero"`
|
||||
// Seed requests deterministic sampling on endpoints that support it.
|
||||
// Equal seeds and parameters do not guarantee equal results.
|
||||
Seed *int `json:"seed,omitempty"`
|
||||
Seed *int `json:"seed,omitzero"`
|
||||
// ServiceTier selects the endpoint's request-processing tier.
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
ServiceTier string `json:"service_tier,omitzero"`
|
||||
// Stop contains sequences that end generation.
|
||||
// The returned text excludes the matched sequence.
|
||||
Stop []string `json:"stop,omitempty"`
|
||||
Stop []string `json:"stop,omitzero"`
|
||||
// Stream requests a sequence of StreamChunk values instead of
|
||||
// ChatResponse.
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Stream bool `json:"stream,omitzero"`
|
||||
// StreamOptions configures a streaming response.
|
||||
// It is meaningful only when Stream is true.
|
||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||
StreamOptions *StreamOptions `json:"stream_options,omitzero"`
|
||||
// Temperature controls sampling randomness.
|
||||
// Nil leaves the choice to the endpoint.
|
||||
Temperature *float32 `json:"temperature,omitempty"`
|
||||
Temperature *float32 `json:"temperature,omitzero"`
|
||||
// ThinkingBudgetTokens is a llama.cpp extension controlling the token
|
||||
// budget for reasoning. Nil omits it, a negative value requests an
|
||||
// unrestricted budget, zero requests an immediate end, and a positive
|
||||
// value sets the budget.
|
||||
ThinkingBudgetTokens *int `json:"thinking_budget_tokens,omitempty"`
|
||||
ThinkingBudgetTokens *int `json:"thinking_budget_tokens,omitzero"`
|
||||
// ToolChoice controls whether the model may or must call a tool.
|
||||
// It accepts a mode string or an endpoint-defined object selecting a
|
||||
// particular tool.
|
||||
ToolChoice any `json:"tool_choice,omitempty"`
|
||||
ToolChoice any `json:"tool_choice,omitzero"`
|
||||
// Tools describes the tools available to the model.
|
||||
Tools []APITool `json:"tools,omitempty"`
|
||||
Tools []APITool `json:"tools,omitzero"`
|
||||
// TopLogProbs is the number of most likely tokens reported at each
|
||||
// position when LogProbs is true.
|
||||
TopLogProbs int `json:"top_logprobs,omitempty"`
|
||||
TopLogProbs int `json:"top_logprobs,omitzero"`
|
||||
// TopP controls nucleus sampling.
|
||||
// Nil leaves the choice to the endpoint.
|
||||
TopP *float32 `json:"top_p,omitempty"`
|
||||
TopP *float32 `json:"top_p,omitzero"`
|
||||
}
|
||||
|
||||
// Metadata describes one selected completion, separately from its message.
|
||||
@@ -327,20 +327,20 @@ type ChatResponse struct {
|
||||
// Choices contains the generated alternatives.
|
||||
Choices []Choice `json:"choices"`
|
||||
// Created is the endpoint's completion creation time in Unix seconds.
|
||||
Created *int64 `json:"created,omitempty"`
|
||||
Created *int64 `json:"created,omitzero"`
|
||||
// ID identifies the completion in the response body, not a request
|
||||
// header.
|
||||
ID *string `json:"id,omitempty"`
|
||||
ID *string `json:"id,omitzero"`
|
||||
// Model identifies the model that generated the response.
|
||||
Model *string `json:"model,omitempty"`
|
||||
Model *string `json:"model,omitzero"`
|
||||
// ServiceTier is the processing tier reported by the endpoint.
|
||||
ServiceTier *string `json:"service_tier,omitempty"`
|
||||
ServiceTier *string `json:"service_tier,omitzero"`
|
||||
// SystemFingerprint identifies the reported backend configuration.
|
||||
SystemFingerprint *string `json:"system_fingerprint,omitempty"`
|
||||
SystemFingerprint *string `json:"system_fingerprint,omitzero"`
|
||||
// Timings contains llama.cpp performance data, when reported.
|
||||
Timings *Timings `json:"timings,omitempty"`
|
||||
Timings *Timings `json:"timings,omitzero"`
|
||||
// Usage contains token counts, when reported.
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
Usage *Usage `json:"usage,omitzero"`
|
||||
}
|
||||
|
||||
// Choice is one generated alternative in a non-streaming response. Index is its
|
||||
@@ -348,12 +348,12 @@ type ChatResponse struct {
|
||||
// ended. Nil metadata fields mean the endpoint did not report those values.
|
||||
type Choice struct {
|
||||
// FinishReason is the endpoint's reason for ending generation.
|
||||
FinishReason *string `json:"finish_reason,omitempty"`
|
||||
FinishReason *string `json:"finish_reason,omitzero"`
|
||||
// Index is the zero-based position of this choice in the response.
|
||||
Index *int `json:"index,omitempty"`
|
||||
Index *int `json:"index,omitzero"`
|
||||
// Logprobs contains probabilities for this choice's content and
|
||||
// refusal.
|
||||
Logprobs *Logprobs `json:"logprobs,omitempty"`
|
||||
Logprobs *Logprobs `json:"logprobs,omitzero"`
|
||||
// Message is the generated message.
|
||||
Message Message `json:"message"`
|
||||
}
|
||||
@@ -379,48 +379,90 @@ const (
|
||||
// Text. The package does not validate that correspondence.
|
||||
type ContentPart struct {
|
||||
// File contains the payload for ContentTypeFile.
|
||||
File *FileInput `json:"file,omitempty"`
|
||||
File *FileInput `json:"file,omitzero"`
|
||||
// ImageURL contains the payload for ContentTypeImageURL.
|
||||
ImageURL *ImageURL `json:"image_url,omitempty"`
|
||||
ImageURL *ImageURL `json:"image_url,omitzero"`
|
||||
// InputAudio contains the payload for ContentTypeInputAudio.
|
||||
InputAudio *InputAudio `json:"input_audio,omitempty"`
|
||||
InputAudio *InputAudio `json:"input_audio,omitzero"`
|
||||
// Refusal contains the payload for ContentTypeRefusal.
|
||||
Refusal string `json:"refusal,omitempty"`
|
||||
Refusal string `json:"refusal,omitzero"`
|
||||
// Text contains the payload for ContentTypeText.
|
||||
Text string `json:"text,omitempty"`
|
||||
Text string `json:"text,omitzero"`
|
||||
// Type identifies which payload field is meaningful.
|
||||
Type ContentType `json:"type"`
|
||||
}
|
||||
|
||||
// MarshalJSON writes the payload field that Type selects, then the type.
|
||||
// The payload field is always written, even when it is empty. The OpenAI
|
||||
// API requires the payload field in every content part, and a text part
|
||||
// without a text key makes llama.cpp reject the request. Struct tags
|
||||
// cannot express this: a field that is required for one part type must be
|
||||
// omitted from the other part types. An unknown Type falls back to the
|
||||
// struct tags.
|
||||
func (p ContentPart) MarshalJSON() ([]byte, error) {
|
||||
switch p.Type {
|
||||
case ContentTypeText:
|
||||
return json.Marshal(struct {
|
||||
Text string `json:"text"`
|
||||
Type ContentType `json:"type"`
|
||||
}{p.Text, p.Type})
|
||||
case ContentTypeRefusal:
|
||||
return json.Marshal(struct {
|
||||
Refusal string `json:"refusal"`
|
||||
Type ContentType `json:"type"`
|
||||
}{p.Refusal, p.Type})
|
||||
case ContentTypeImageURL:
|
||||
return json.Marshal(struct {
|
||||
ImageURL *ImageURL `json:"image_url"`
|
||||
Type ContentType `json:"type"`
|
||||
}{p.ImageURL, p.Type})
|
||||
case ContentTypeInputAudio:
|
||||
return json.Marshal(struct {
|
||||
InputAudio *InputAudio `json:"input_audio"`
|
||||
Type ContentType `json:"type"`
|
||||
}{p.InputAudio, p.Type})
|
||||
case ContentTypeFile:
|
||||
return json.Marshal(struct {
|
||||
File *FileInput `json:"file"`
|
||||
Type ContentType `json:"type"`
|
||||
}{p.File, p.Type})
|
||||
}
|
||||
// Unknown types keep the struct-tag encoding. The method must not
|
||||
// reject them: the endpoint, not this client, defines which types
|
||||
// are valid.
|
||||
type alias ContentPart
|
||||
return json.Marshal(alias(p))
|
||||
}
|
||||
|
||||
// Delta is an incremental message update in a StreamChunk. Reasoning and
|
||||
// ReasoningContent are alternative field names used by compatible endpoints;
|
||||
// StreamAccumulator preserves fragments from both.
|
||||
type Delta struct {
|
||||
// Content is the next fragment of message text.
|
||||
Content string `json:"content,omitempty"`
|
||||
Content string `json:"content,omitzero"`
|
||||
// Reasoning is the next reasoning fragment under the reasoning field
|
||||
// name.
|
||||
Reasoning string `json:"reasoning,omitempty"`
|
||||
Reasoning string `json:"reasoning,omitzero"`
|
||||
// ReasoningContent is the next reasoning fragment under the
|
||||
// reasoning_content field name.
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitzero"`
|
||||
// Refusal is the next fragment of refusal text.
|
||||
Refusal string `json:"refusal,omitempty"`
|
||||
Refusal string `json:"refusal,omitzero"`
|
||||
// Role identifies the message role when supplied by the endpoint.
|
||||
Role Role `json:"role,omitempty"`
|
||||
Role Role `json:"role,omitzero"`
|
||||
// ToolCalls contains the next fragments of one or more tool calls.
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitzero"`
|
||||
}
|
||||
|
||||
// FileInput identifies file content either inline through FileData or by a
|
||||
// server-assigned FileID.
|
||||
type FileInput struct {
|
||||
// FileData is the base64-encoded contents of an inline file.
|
||||
FileData string `json:"file_data,omitempty"`
|
||||
FileData string `json:"file_data,omitzero"`
|
||||
// FileID references a file previously uploaded to the server.
|
||||
FileID string `json:"file_id,omitempty"`
|
||||
FileID string `json:"file_id,omitzero"`
|
||||
// Filename names an inline file supplied through FileData.
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Filename string `json:"filename,omitzero"`
|
||||
}
|
||||
|
||||
// Function identifies a called function and carries its arguments as a JSON
|
||||
@@ -428,15 +470,15 @@ type FileInput struct {
|
||||
// before invoking the function.
|
||||
type Function struct {
|
||||
// Arguments is the JSON-encoded argument object generated by the model.
|
||||
Arguments string `json:"arguments,omitempty"`
|
||||
Arguments string `json:"arguments,omitzero"`
|
||||
// Name identifies the function to call.
|
||||
Name string `json:"name,omitempty"`
|
||||
Name string `json:"name,omitzero"`
|
||||
}
|
||||
|
||||
// FunctionDef declares a function the model may call.
|
||||
type FunctionDef struct {
|
||||
// Description states what the function does and when to call it.
|
||||
Description string `json:"description,omitempty"`
|
||||
Description string `json:"description,omitzero"`
|
||||
// Name identifies the function.
|
||||
Name string `json:"name"`
|
||||
// Parameters is a JSON Schema describing the arguments accepted by the
|
||||
@@ -444,7 +486,7 @@ type FunctionDef struct {
|
||||
Parameters any `json:"parameters"`
|
||||
// Strict requests exact conformance to Parameters, where the server
|
||||
// supports it.
|
||||
Strict bool `json:"strict,omitempty"`
|
||||
Strict bool `json:"strict,omitzero"`
|
||||
}
|
||||
|
||||
// ImageDetail selects the fidelity used when processing an image. Auto lets the
|
||||
@@ -460,7 +502,7 @@ const (
|
||||
// ImageURL carries an image content part.
|
||||
type ImageURL struct {
|
||||
// Detail is optional; when omitted the server chooses.
|
||||
Detail ImageDetail `json:"detail,omitempty"`
|
||||
Detail ImageDetail `json:"detail,omitzero"`
|
||||
// URL is a public image URL or a base64 data URI.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
@@ -494,23 +536,23 @@ type InputAudio struct {
|
||||
type Message struct {
|
||||
// ContentParts holds the message content in parts; it is the
|
||||
// in-memory form of the JSON "content" field.
|
||||
ContentParts []ContentPart `json:"content,omitempty"`
|
||||
ContentParts []ContentPart `json:"content,omitzero"`
|
||||
// Name optionally identifies the sender.
|
||||
Name string `json:"name,omitempty"`
|
||||
Name string `json:"name,omitzero"`
|
||||
// Reasoning and ReasoningContent are alternative wire fields used by
|
||||
// compatible servers. After unmarshalling, ReasoningContent contains
|
||||
// either representation, with reasoning_content taking precedence when
|
||||
// both occur.
|
||||
Reasoning string `json:"reasoning,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
Reasoning string `json:"reasoning,omitzero"`
|
||||
ReasoningContent string `json:"reasoning_content,omitzero"`
|
||||
// Refusal contains the endpoint's refusal text, when present.
|
||||
Refusal string `json:"refusal,omitempty"`
|
||||
Refusal string `json:"refusal,omitzero"`
|
||||
// Role identifies the author or purpose of the message.
|
||||
Role Role `json:"role"`
|
||||
// ToolCallID links a tool response to the call it answers.
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitzero"`
|
||||
// ToolCalls contains calls requested by an assistant message.
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitzero"`
|
||||
}
|
||||
|
||||
// Text concatenates the Text payload of every text content part in order. It
|
||||
@@ -628,13 +670,13 @@ func (m *Message) UnmarshalJSON(data []byte) error {
|
||||
// [OpenAI Models API]: https://platform.openai.com/docs/api-reference/models/list
|
||||
type Model struct {
|
||||
// ContextLength is the model's context window in tokens, when reported.
|
||||
ContextLength int `json:"context_length,omitempty"`
|
||||
ContextLength int `json:"context_length,omitzero"`
|
||||
// Created is the model's creation time as Unix seconds.
|
||||
Created int64 `json:"created"`
|
||||
// ID identifies the model in inference requests.
|
||||
ID string `json:"id"`
|
||||
// Meta contains endpoint-specific model metadata, when reported.
|
||||
Meta *ModelMeta `json:"meta,omitempty"`
|
||||
Meta *ModelMeta `json:"meta,omitzero"`
|
||||
// Object is the endpoint's object type for the model entry.
|
||||
Object string `json:"object"`
|
||||
// OwnedBy identifies the organization that owns the model, when
|
||||
@@ -650,9 +692,9 @@ type Model struct {
|
||||
// [llama.cpp API documentation]: https://llama.app/docs/api
|
||||
type ModelMeta struct {
|
||||
// NCtx is the context size with which llama.cpp loaded the model.
|
||||
NCtx int `json:"n_ctx,omitempty"`
|
||||
NCtx int `json:"n_ctx,omitzero"`
|
||||
// NCtxTrain is the model's training context size.
|
||||
NCtxTrain int `json:"n_ctx_train,omitempty"`
|
||||
NCtxTrain int `json:"n_ctx_train,omitzero"`
|
||||
}
|
||||
|
||||
// ModelsResponse is the response body of GET /models.
|
||||
@@ -678,7 +720,7 @@ const (
|
||||
type ResponseFormat struct {
|
||||
// JSONSchema defines the required output schema when Type is
|
||||
// json_schema.
|
||||
JSONSchema any `json:"json_schema,omitempty"`
|
||||
JSONSchema any `json:"json_schema,omitzero"`
|
||||
// Type selects text, json_object, or json_schema output.
|
||||
Type string `json:"type"`
|
||||
}
|
||||
@@ -695,29 +737,29 @@ type StreamChunk struct {
|
||||
Delta Delta `json:"delta"`
|
||||
// FinishReason is the reported reason for ending this choice;
|
||||
// it does not establish receipt of DONE.
|
||||
FinishReason *string `json:"finish_reason,omitempty"`
|
||||
FinishReason *string `json:"finish_reason,omitzero"`
|
||||
// Index identifies the choice updated by this delta. Nil means
|
||||
// unreported and selects the first choice for accumulation.
|
||||
Index *int `json:"index,omitempty"`
|
||||
Index *int `json:"index,omitzero"`
|
||||
// Logprobs contains the next content and refusal token
|
||||
// probabilities.
|
||||
Logprobs *Logprobs `json:"logprobs,omitempty"`
|
||||
Logprobs *Logprobs `json:"logprobs,omitzero"`
|
||||
} `json:"choices"`
|
||||
// Created is the endpoint's completion creation time in Unix seconds.
|
||||
Created *int64 `json:"created,omitempty"`
|
||||
Created *int64 `json:"created,omitzero"`
|
||||
// ID identifies the completion in the response body, not a request
|
||||
// header.
|
||||
ID *string `json:"id,omitempty"`
|
||||
ID *string `json:"id,omitzero"`
|
||||
// Model identifies the model generating the response.
|
||||
Model *string `json:"model,omitempty"`
|
||||
Model *string `json:"model,omitzero"`
|
||||
// ServiceTier is the processing tier reported by the endpoint.
|
||||
ServiceTier *string `json:"service_tier,omitempty"`
|
||||
ServiceTier *string `json:"service_tier,omitzero"`
|
||||
// SystemFingerprint identifies the reported backend configuration.
|
||||
SystemFingerprint *string `json:"system_fingerprint,omitempty"`
|
||||
SystemFingerprint *string `json:"system_fingerprint,omitzero"`
|
||||
// Timings contains llama.cpp performance data, when reported.
|
||||
Timings *Timings `json:"timings,omitempty"`
|
||||
Timings *Timings `json:"timings,omitzero"`
|
||||
// Usage contains aggregate token counts, when reported.
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
Usage *Usage `json:"usage,omitzero"`
|
||||
}
|
||||
|
||||
// StreamOptions configures a streaming response.
|
||||
@@ -725,7 +767,7 @@ type StreamOptions struct {
|
||||
// IncludeUsage requests an additional chunk containing aggregate token
|
||||
// usage before the terminal data marker. If the stream is interrupted,
|
||||
// that chunk may not arrive.
|
||||
IncludeUsage bool `json:"include_usage,omitempty"`
|
||||
IncludeUsage bool `json:"include_usage,omitzero"`
|
||||
}
|
||||
|
||||
// ToolCall is one tool call requested by the model.
|
||||
@@ -734,10 +776,10 @@ type ToolCall struct {
|
||||
Function Function `json:"function"`
|
||||
// ID identifies the call. The corresponding tool response carries the
|
||||
// same value in Message.ToolCallID.
|
||||
ID string `json:"id,omitempty"`
|
||||
ID string `json:"id,omitzero"`
|
||||
// Index identifies the call whose fragments a streaming delta
|
||||
// contains. It is omitted from non-streaming responses.
|
||||
Index *int `json:"index,omitempty"`
|
||||
Index *int `json:"index,omitzero"`
|
||||
// Type identifies the kind of tool being called.
|
||||
Type ToolType `json:"type"`
|
||||
}
|
||||
|
||||
@@ -34,6 +34,17 @@ func TestContentPartMarshalJSON(t *testing.T) {
|
||||
},
|
||||
want: `{"refusal":"cannot comply","type":"refusal"}`,
|
||||
},
|
||||
{
|
||||
// The payload field is required, also when it is empty.
|
||||
name: "empty text",
|
||||
part: ContentPart{Type: ContentTypeText},
|
||||
want: `{"text":"","type":"text"}`,
|
||||
},
|
||||
{
|
||||
name: "empty refusal",
|
||||
part: ContentPart{Type: ContentTypeRefusal},
|
||||
want: `{"refusal":"","type":"refusal"}`,
|
||||
},
|
||||
{
|
||||
name: "image URL",
|
||||
part: ContentPart{
|
||||
@@ -157,3 +168,102 @@ func TestMessageUnmarshalJSONReplacesArrayContent(t *testing.T) {
|
||||
t.Errorf("Message.Text() = %q, want %q", got, "new")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMessageEmptyTextRoundTrip is a regression test. ContentPart.Text used
|
||||
// the omitzero option, so a text part with empty text marshaled as
|
||||
// {"type":"text"}, without a text key. The message was stored in that form
|
||||
// and sent to the model server on the next inference, and the server
|
||||
// rejected the request: the OpenAI API requires the text field, and
|
||||
// llama.cpp fails to parse the part. MarshalJSON now always writes the
|
||||
// payload field. Rows stored in the old form are corrected when they are
|
||||
// decoded and encoded again.
|
||||
func TestMessageEmptyTextRoundTrip(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
// Model replies that contain only tool calls can send
|
||||
// an empty content string.
|
||||
name: "empty content string",
|
||||
input: `{"role":"assistant","content":""}`,
|
||||
want: `{"content":[{"text":"","type":"text"}],"role":"assistant"}`,
|
||||
},
|
||||
{
|
||||
// The UI sends this when the user edits a message's
|
||||
// text to empty.
|
||||
name: "empty text part",
|
||||
input: `{"role":"user","content":[{"type":"text","text":""}]}`,
|
||||
want: `{"content":[{"text":"","type":"text"}],"role":"user"}`,
|
||||
},
|
||||
{
|
||||
// Rows stored before the fix have this form.
|
||||
name: "text part without text key",
|
||||
input: `{"role":"user","content":[{"type":"text"}]}`,
|
||||
want: `{"content":[{"text":"","type":"text"}],"role":"user"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var message Message
|
||||
if err := json.Unmarshal([]byte(tt.input), &message); err != nil {
|
||||
t.Fatalf("json.Unmarshal() error = %v", err)
|
||||
}
|
||||
got, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal() error = %v", err)
|
||||
}
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("json.Marshal() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResponseFormatMarshalJSON records the schema-omission policy of
|
||||
// ResponseFormat. The omitzero option drops only a nil schema. An empty
|
||||
// JSON schema object is valid and permits any output, so it must be
|
||||
// sent, not dropped. The omitempty option would drop it.
|
||||
func TestResponseFormatMarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format ResponseFormat
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil schema",
|
||||
format: ResponseFormat{Type: "json_object"},
|
||||
want: `{"type":"json_object"}`,
|
||||
},
|
||||
{
|
||||
name: "schema object",
|
||||
format: ResponseFormat{
|
||||
JSONSchema: map[string]any{"name": "calendar"},
|
||||
Type: "json_schema",
|
||||
},
|
||||
want: `{"json_schema":{"name":"calendar"},"type":"json_schema"}`,
|
||||
},
|
||||
{
|
||||
name: "empty schema object",
|
||||
format: ResponseFormat{
|
||||
JSONSchema: map[string]any{},
|
||||
Type: "json_schema",
|
||||
},
|
||||
want: `{"json_schema":{},"type":"json_schema"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := json.Marshal(tt.format)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal() error = %v", err)
|
||||
}
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("json.Marshal() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user