// 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 == endpointAPI {
+ if config.Endpoint == configEndpointAPI {
key := os.Getenv("OPENAI_API_KEY")
if key == "" {
return "", nil, errors.New("endpoint api needs OPENAI_API_KEY")
// modelUserMessage returns a user input item with one text part for each text.
func modelUserMessage(texts ...string) any {
- type part struct {
- Type string `json:"type"`
- Text string `json:"text"`
- }
-
- content := make([]part, len(texts))
+ content := make([]ModelInputPart, len(texts))
for n, text := range texts {
- content[n] = part{
+ 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 []part `json:"content"`
+ 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)
// 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()
}
// 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
// agent, so the turns and the compactions of the agent share one prompt cache.
-// The ChatGPT backend caches only with the key in the session_id header, so a
-// call to it sends the key there and in the body. Every call is recorded in the
-// usage file.
+// A call of a subagent sends the key of the agent with "-" and the name of the
+// subagent, so that the calls of a subagent do not share a cache entry with the
+// calls of the agent that run at the same time. The ChatGPT backend caches only
+// with the key in the session_id header, so a call to it sends the key there
+// and in the body. Every call is recorded in the usage file.
//
// The request never holds max_output_tokens or metadata, because the ChatGPT
// backend refuses them. It sets store to false, so the client sends the whole
return c.limit > 0 && c.calls.Load() >= c.limit
}
+// CallLimit returns the configured call limit, or zero for no limit.
+func (c *ModelClient) CallLimit() int64 {
+ return c.limit
+}
+
+// UsagePath returns the file that records the calls of this client.
+func (c *ModelClient) UsagePath() string {
+ return c.usage.path
+}
+
// Create sends the call and reads its event stream to the end. Over the call
// limit it sends nothing and returns errModelCallLimit.
func (c *ModelClient) Create(ctx context.Context, request ModelRequest) (ModelResponse, error) {
return ModelResponse{}, errModelCallLimit
}
+ cacheKey := c.cacheKey
+ if request.Agent != "" {
+ cacheKey += "-" + request.Agent
+ }
+
body, err := json.Marshal(ModelRequestBody{
Model: request.Model,
Instructions: request.Instructions,
Store: false,
Stream: true,
Include: []string{"reasoning.encrypted_content"},
- PromptCacheKey: c.cacheKey,
+ PromptCacheKey: cacheKey,
})
if err != nil {
return ModelResponse{}, fmt.Errorf("encode request: %w", err)
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("Accept", "text/event-stream")
+
err = c.auth.Authorize(ctx, httpRequest.Header)
if err != nil {
return ModelResponse{}, fmt.Errorf("authorize model call: %w", err)
_, chatgpt := c.auth.(*ChatGPTAuth)
if chatgpt {
- httpRequest.Header.Set("session_id", c.cacheKey)
+ httpRequest.Header.Set("session_id", cacheKey)
}
start := time.Now()
}
record.Turn = request.Turn
+ record.Agent = request.Agent
response, err := c.send(httpRequest, &record)
if err != nil {
record.Error = err.Error()
} `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. Node and Turn go only to the usage file, never to the
-// endpoint.
+// 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
+ Node *NodeKey
Turn string
+ Agent string
}
// ModelResponse is the result of a model call. Output holds the output items as
// 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. Quota holds the
-// x-codex-* headers of the response, without the turn state.
+// 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"`
// without the turn state.
func modelQuotaHeaders(header http.Header) map[string]string {
var quota map[string]string
+
for name, values := range header {
if !strings.HasPrefix(name, modelQuotaPrefix) || name == modelTurnStateHeader || len(values) == 0 {
continue
// the completed response when the stream sent none.
func modelReadStream(stream io.Reader) (ModelResponse, error) {
var response ModelResponse
+
reader := bufio.NewReader(stream)
+
var data bytes.Buffer
+
for {
line, err := reader.ReadBytes('\n')
if err != nil && !errors.Is(err, io.EOF) {
} else if data.Len() > 0 {
done, eventErr := modelReadEvent(&response, data.Bytes())
data.Reset()
+
if eventErr != nil || done {
return response, eventErr
}
// stream.
func modelReadEvent(response *ModelResponse, data []byte) (bool, error) {
var event ModelStreamEvent
+
err := json.Unmarshal(data, &event)
if err != nil {
return true, fmt.Errorf("decode stream event: %w", err)