// errModelCallLimit tells that a client sent its limit of calls.
var errModelCallLimit = errors.New("call limit reached")
-// modelEndpoint returns the URL and the authorizer of the endpoint in config.
-// The ChatGPT backend uses the token file in dir. The OpenAI API uses the key
-// in OPENAI_API_KEY, and there is no fallback from one to the other. All
-// clients share the authorizer, so one refresh runs at a time.
-func modelEndpoint(config Config, dir string) (string, ModelAuthorizer, error) {
- if config.Endpoint == configEndpointAPI {
- key := os.Getenv("OPENAI_API_KEY")
- if key == "" {
- return "", nil, errors.New("endpoint api needs OPENAI_API_KEY")
- }
-
- return modelAPIURL, ModelAPIKey(key), nil
- }
-
- return modelChatGPTURL, newChatGPTAuth(filepath.Join(dir, chatgptTokenFileName), chatgptIssuer), nil
-}
-
-// newModelClient returns a client for the Responses endpoint at url. It records
-// each call in the usage file at usagePath.
-func newModelClient(url string, auth ModelAuthorizer, cacheKey, usagePath string) *ModelClient {
- return &ModelClient{
- url: url,
- auth: auth,
- cacheKey: cacheKey,
- usage: &ModelUsageLog{
- path: usagePath,
- },
- http: &http.Client{},
- }
-}
-
-// modelUserMessage returns a user input item with one text part for each text.
-func modelUserMessage(texts ...string) any {
- content := make([]ModelInputPart, len(texts))
- for n, text := range texts {
- content[n] = ModelInputPart{
- Type: "input_text",
- Text: text,
- }
- }
-
- return modelUserParts(content)
-}
-
-// modelUserParts keeps text and image parts in one user message.
-func modelUserParts(content []ModelInputPart) any {
- return struct {
- Role string `json:"role"`
- Content []ModelInputPart `json:"content"`
- }{
- Role: "user",
- Content: content,
- }
-}
-
-// modelOutputText returns the text of the message items of output.
-func modelOutputText(output []json.RawMessage) (string, error) {
- var text strings.Builder
-
- for _, raw := range output {
- var item ModelOutputItem
-
- err := json.Unmarshal(raw, &item)
- if err != nil {
- return "", fmt.Errorf("decode output item: %w", err)
- }
-
- if item.Type != "message" {
- continue
- }
-
- for _, part := range item.Content {
- if part.Type == "output_text" {
- text.WriteString(part.Text)
- }
- }
- }
-
- return text.String(), nil
-}
-
// ModelAuthorizer adds the credentials of a model call to the headers of its
// request.
type ModelAuthorizer interface {
// ModelAPIKey authorizes calls to the OpenAI API with a key.
type ModelAPIKey string
-// Authorize sets the key as the bearer token.
-func (k ModelAPIKey) Authorize(ctx context.Context, header http.Header) error {
- header.Set("Authorization", "Bearer "+string(k))
-
- return ctx.Err()
-}
-
// ModelClient sends model calls to a Responses endpoint for one agent. The
// ChatGPT backend and the OpenAI API take the same request body and differ only
// in the URL and the credentials. Every call sends the fixed cache key of the
calls atomic.Int64
}
+// ModelUsageLog appends one JSON line for each model call to usage.jsonl. Calls
+// can run at once, so a mutex serializes the writes.
+type ModelUsageLog struct {
+ mutex sync.Mutex
+ path string
+}
+
+// ModelOutputItem holds the fields of an output item that give its text.
+type ModelOutputItem struct {
+ Type string `json:"type"`
+ Content []struct {
+ Type string `json:"type"`
+ Text string `json:"text"`
+ } `json:"content"`
+}
+
+// ModelInputPart is a text or image part in a user message or tool result.
+type ModelInputPart struct {
+ Type string `json:"type"`
+ Text string `json:"text,omitempty"`
+ ImageURL string `json:"image_url,omitempty"`
+}
+
+// ModelRequest is one model call. Input holds the input items in order, such as
+// modelUserMessage values or output items of an earlier response. Node names
+// the node that a compaction builds, or is nil. Turn names the request of a
+// turn: first or next. Agent names the subagent that makes the call, or is
+// empty for a call of the agent itself. Node, Turn, and Agent go to the usage
+// file, never to the endpoint as fields. Agent also gives the call the cache
+// key of its subagent.
+type ModelRequest struct {
+ Model string
+ Effort string
+ Instructions string
+ Tools []ModelTool
+ Input []any
+ Node *NodeKey
+ Turn string
+ Agent string
+}
+
+// ModelResponse is the result of a model call. Output holds the output items as
+// the endpoint sent them. Text is the text of the message items.
+type ModelResponse struct {
+ Output []json.RawMessage
+ Text string
+ Usage ModelUsage
+}
+
+// ModelRequestBody is the JSON body of a call.
+type ModelRequestBody struct {
+ Model string `json:"model"`
+ Instructions string `json:"instructions"`
+ Input []any `json:"input"`
+ Tools []ModelTool `json:"tools"`
+ Reasoning ModelReasoning `json:"reasoning"`
+ Store bool `json:"store"`
+ Stream bool `json:"stream"`
+ Include []string `json:"include"`
+ PromptCacheKey string `json:"prompt_cache_key"`
+}
+
+type ModelReasoning struct {
+ Effort string `json:"effort"`
+}
+
+// ModelUsageRecord is one line of usage.jsonl. Date is the start of the call,
+// and Seconds its length. Node is the node [l, i] of a compaction. Status is
+// the HTTP status, or 0 when no response came. TextBytes is the size of the
+// text of the reply. Turn names the request of a turn. Agent names the subagent
+// of the call. Quota holds the x-codex-* headers of the response, without the
+// turn state.
+type ModelUsageRecord struct {
+ Date string `json:"date"`
+ Seconds float64 `json:"seconds"`
+ Model string `json:"model"`
+ Node *[2]int `json:"node,omitempty"`
+ Turn string `json:"turn,omitempty"`
+ Agent string `json:"agent,omitempty"`
+ Status int `json:"status"`
+ InputTokens int `json:"input_tokens"`
+ CachedTokens int `json:"cached_tokens"`
+ OutputTokens int `json:"output_tokens"`
+ TextBytes int `json:"text_bytes"`
+ Quota map[string]string `json:"quota,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+// ModelTool is the definition of a function tool.
+type ModelTool struct {
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Parameters any `json:"parameters"`
+ Strict bool `json:"strict"`
+}
+
+// ModelUsage gives the token counts of a call. Cached is the part of the input
+// tokens that came from the prompt cache.
+type ModelUsage struct {
+ Input int
+ Cached int
+ Output int
+}
+
+// ModelStreamEvent holds the fields of the stream events that the client reads.
+type ModelStreamEvent struct {
+ Type string `json:"type"`
+ Item json.RawMessage `json:"item"`
+ Response *ModelEventResponse `json:"response"`
+ Code string `json:"code"`
+ Message string `json:"message"`
+}
+
+type ModelEventResponse struct {
+ Output []json.RawMessage `json:"output"`
+ Usage *ModelWireUsage `json:"usage"`
+ Error *ModelWireError `json:"error"`
+ IncompleteDetails *struct {
+ Reason string `json:"reason"`
+ } `json:"incomplete_details"`
+}
+
+type ModelWireUsage struct {
+ InputTokens int `json:"input_tokens"`
+ OutputTokens int `json:"output_tokens"`
+ InputTokensDetails struct {
+ CachedTokens int `json:"cached_tokens"`
+ } `json:"input_tokens_details"`
+}
+
+type ModelWireError struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+}
+
+// newModelClient returns a client for the Responses endpoint at url. It records
+// each call in the usage file at usagePath.
+func newModelClient(url string, auth ModelAuthorizer, cacheKey, usagePath string) *ModelClient {
+ return &ModelClient{
+ url: url,
+ auth: auth,
+ cacheKey: cacheKey,
+ usage: &ModelUsageLog{
+ path: usagePath,
+ },
+ http: &http.Client{},
+ }
+}
+
+// Authorize sets the key as the bearer token.
+func (k ModelAPIKey) Authorize(ctx context.Context, header http.Header) error {
+ header.Set("Authorization", "Bearer "+string(k))
+
+ return ctx.Err()
+}
+
// LimitCalls makes the client refuse each call after the first limit calls.
func (c *ModelClient) LimitCalls(limit int64) {
c.limit = limit
return modelReadStream(httpResponse.Body)
}
-// ModelUsageLog appends one JSON line for each model call to usage.jsonl. Calls
-// can run at once, so a mutex serializes the writes.
-type ModelUsageLog struct {
- mutex sync.Mutex
- path string
-}
-
// Write appends record to the file with one write.
func (u *ModelUsageLog) Write(record ModelUsageRecord) (err error) {
line, err := json.Marshal(record)
return nil
}
-// ModelOutputItem holds the fields of an output item that give its text.
-type ModelOutputItem struct {
- Type string `json:"type"`
- Content []struct {
- Type string `json:"type"`
- Text string `json:"text"`
- } `json:"content"`
-}
-
-// ModelInputPart is a text or image part in a user message or tool result.
-type ModelInputPart struct {
- Type string `json:"type"`
- Text string `json:"text,omitempty"`
- ImageURL string `json:"image_url,omitempty"`
-}
+// modelEndpoint returns the URL and the authorizer of the endpoint in config.
+// The ChatGPT backend uses the token file in dir. The OpenAI API uses the key
+// in OPENAI_API_KEY, and there is no fallback from one to the other. All
+// clients share the authorizer, so one refresh runs at a time.
+func modelEndpoint(config Config, dir string) (string, ModelAuthorizer, error) {
+ if config.Endpoint == configEndpointAPI {
+ key := os.Getenv("OPENAI_API_KEY")
+ if key == "" {
+ return "", nil, errors.New("endpoint api needs OPENAI_API_KEY")
+ }
-// ModelRequest is one model call. Input holds the input items in order, such as
-// modelUserMessage values or output items of an earlier response. Node names
-// the node that a compaction builds, or is nil. Turn names the request of a
-// turn: first or next. Agent names the subagent that makes the call, or is
-// empty for a call of the agent itself. Node, Turn, and Agent go to the usage
-// file, never to the endpoint as fields. Agent also gives the call the cache
-// key of its subagent.
-type ModelRequest struct {
- Model string
- Effort string
- Instructions string
- Tools []ModelTool
- Input []any
- Node *NodeKey
- Turn string
- Agent string
-}
+ return modelAPIURL, ModelAPIKey(key), nil
+ }
-// ModelResponse is the result of a model call. Output holds the output items as
-// the endpoint sent them. Text is the text of the message items.
-type ModelResponse struct {
- Output []json.RawMessage
- Text string
- Usage ModelUsage
+ return modelChatGPTURL, newChatGPTAuth(filepath.Join(dir, chatgptTokenFileName), chatgptIssuer), nil
}
-// ModelRequestBody is the JSON body of a call.
-type ModelRequestBody struct {
- Model string `json:"model"`
- Instructions string `json:"instructions"`
- Input []any `json:"input"`
- Tools []ModelTool `json:"tools"`
- Reasoning ModelReasoning `json:"reasoning"`
- Store bool `json:"store"`
- Stream bool `json:"stream"`
- Include []string `json:"include"`
- PromptCacheKey string `json:"prompt_cache_key"`
-}
+// modelUserMessage returns a user input item with one text part for each text.
+func modelUserMessage(texts ...string) any {
+ content := make([]ModelInputPart, len(texts))
+ for n, text := range texts {
+ content[n] = ModelInputPart{
+ Type: "input_text",
+ Text: text,
+ }
+ }
-type ModelReasoning struct {
- Effort string `json:"effort"`
+ return modelUserParts(content)
}
-// ModelUsageRecord is one line of usage.jsonl. Date is the start of the call,
-// and Seconds its length. Node is the node [l, i] of a compaction. Status is
-// the HTTP status, or 0 when no response came. TextBytes is the size of the
-// text of the reply. Turn names the request of a turn. Agent names the subagent
-// of the call. Quota holds the x-codex-* headers of the response, without the
-// turn state.
-type ModelUsageRecord struct {
- Date string `json:"date"`
- Seconds float64 `json:"seconds"`
- Model string `json:"model"`
- Node *[2]int `json:"node,omitempty"`
- Turn string `json:"turn,omitempty"`
- Agent string `json:"agent,omitempty"`
- Status int `json:"status"`
- InputTokens int `json:"input_tokens"`
- CachedTokens int `json:"cached_tokens"`
- OutputTokens int `json:"output_tokens"`
- TextBytes int `json:"text_bytes"`
- Quota map[string]string `json:"quota,omitempty"`
- Error string `json:"error,omitempty"`
+// modelUserParts keeps text and image parts in one user message.
+func modelUserParts(content []ModelInputPart) any {
+ return struct {
+ Role string `json:"role"`
+ Content []ModelInputPart `json:"content"`
+ }{
+ Role: "user",
+ Content: content,
+ }
}
-// ModelTool is the definition of a function tool.
-type ModelTool struct {
- Type string `json:"type"`
- Name string `json:"name"`
- Description string `json:"description"`
- Parameters any `json:"parameters"`
- Strict bool `json:"strict"`
-}
+// modelOutputText returns the text of the message items of output.
+func modelOutputText(output []json.RawMessage) (string, error) {
+ var text strings.Builder
-// ModelUsage gives the token counts of a call. Cached is the part of the input
-// tokens that came from the prompt cache.
-type ModelUsage struct {
- Input int
- Cached int
- Output int
-}
+ for _, raw := range output {
+ var item ModelOutputItem
-// ModelStreamEvent holds the fields of the stream events that the client reads.
-type ModelStreamEvent struct {
- Type string `json:"type"`
- Item json.RawMessage `json:"item"`
- Response *ModelEventResponse `json:"response"`
- Code string `json:"code"`
- Message string `json:"message"`
-}
+ err := json.Unmarshal(raw, &item)
+ if err != nil {
+ return "", fmt.Errorf("decode output item: %w", err)
+ }
-type ModelEventResponse struct {
- Output []json.RawMessage `json:"output"`
- Usage *ModelWireUsage `json:"usage"`
- Error *ModelWireError `json:"error"`
- IncompleteDetails *struct {
- Reason string `json:"reason"`
- } `json:"incomplete_details"`
-}
+ if item.Type != "message" {
+ continue
+ }
-type ModelWireUsage struct {
- InputTokens int `json:"input_tokens"`
- OutputTokens int `json:"output_tokens"`
- InputTokensDetails struct {
- CachedTokens int `json:"cached_tokens"`
- } `json:"input_tokens_details"`
-}
+ for _, part := range item.Content {
+ if part.Type == "output_text" {
+ text.WriteString(part.Text)
+ }
+ }
+ }
-type ModelWireError struct {
- Code string `json:"code"`
- Message string `json:"message"`
+ return text.String(), nil
}
// modelQuotaHeaders returns the x-codex-* headers, with lowercase names,