]> git.giacomolli.uk - onechat/blobdiff - model.go
Name timeouts for the operations they limit
[onechat] / model.go
index 04b15563304af5720269f7a72272a41382952c95..6ddd3008cca12c96577ccc5a9a8b6ca913ebfe7e 100644 (file)
--- a/model.go
+++ b/model.go
@@ -73,22 +73,22 @@ func newModelClient(url string, auth ModelAuthorizer, cacheKey, usagePath string
 
 // modelUserMessage returns a user input item with one text part for each text.
 func modelUserMessage(texts ...string) any {
 
 // 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 {
        for n, text := range texts {
-               content[n] = part{
+               content[n] = ModelInputPart{
                        Type: "input_text",
                        Text: text,
                }
        }
 
                        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 {
        return struct {
-               Role    string `json:"role"`
-               Content []part `json:"content"`
+               Role    string           `json:"role"`
+               Content []ModelInputPart `json:"content"`
        }{
                Role:    "user",
                Content: content,
        }{
                Role:    "user",
                Content: content,
@@ -98,8 +98,10 @@ func modelUserMessage(texts ...string) any {
 // modelOutputText returns the text of the message items of output.
 func modelOutputText(output []json.RawMessage) (string, error) {
        var text strings.Builder
 // 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
        for _, raw := range output {
                var item ModelOutputItem
+
                err := json.Unmarshal(raw, &item)
                if err != nil {
                        return "", fmt.Errorf("decode output item: %w", err)
                err := json.Unmarshal(raw, &item)
                if err != nil {
                        return "", fmt.Errorf("decode output item: %w", err)
@@ -131,6 +133,7 @@ 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))
 // 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()
 }
 
        return ctx.Err()
 }
 
@@ -138,9 +141,11 @@ func (k ModelAPIKey) Authorize(ctx context.Context, header http.Header) error {
 // 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.
 // 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
 //
 // 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
@@ -166,6 +171,16 @@ func (c *ModelClient) LimitReached() bool {
        return c.limit > 0 && c.calls.Load() >= c.limit
 }
 
        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) {
 // 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) {
@@ -173,6 +188,11 @@ func (c *ModelClient) Create(ctx context.Context, request ModelRequest) (ModelRe
                return ModelResponse{}, errModelCallLimit
        }
 
                return ModelResponse{}, errModelCallLimit
        }
 
+       cacheKey := c.cacheKey
+       if request.Agent != "" {
+               cacheKey += "-" + request.Agent
+       }
+
        body, err := json.Marshal(ModelRequestBody{
                Model:        request.Model,
                Instructions: request.Instructions,
        body, err := json.Marshal(ModelRequestBody{
                Model:        request.Model,
                Instructions: request.Instructions,
@@ -184,7 +204,7 @@ func (c *ModelClient) Create(ctx context.Context, request ModelRequest) (ModelRe
                Store:          false,
                Stream:         true,
                Include:        []string{"reasoning.encrypted_content"},
                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)
        })
        if err != nil {
                return ModelResponse{}, fmt.Errorf("encode request: %w", err)
@@ -197,6 +217,7 @@ func (c *ModelClient) Create(ctx context.Context, request ModelRequest) (ModelRe
 
        httpRequest.Header.Set("Content-Type", "application/json")
        httpRequest.Header.Set("Accept", "text/event-stream")
 
        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)
        err = c.auth.Authorize(ctx, httpRequest.Header)
        if err != nil {
                return ModelResponse{}, fmt.Errorf("authorize model call: %w", err)
@@ -204,7 +225,7 @@ func (c *ModelClient) Create(ctx context.Context, request ModelRequest) (ModelRe
 
        _, chatgpt := c.auth.(*ChatGPTAuth)
        if chatgpt {
 
        _, chatgpt := c.auth.(*ChatGPTAuth)
        if chatgpt {
-               httpRequest.Header.Set("session_id", c.cacheKey)
+               httpRequest.Header.Set("session_id", cacheKey)
        }
 
        start := time.Now()
        }
 
        start := time.Now()
@@ -218,6 +239,7 @@ func (c *ModelClient) Create(ctx context.Context, request ModelRequest) (ModelRe
        }
 
        record.Turn = request.Turn
        }
 
        record.Turn = request.Turn
+       record.Agent = request.Agent
        response, err := c.send(httpRequest, &record)
        if err != nil {
                record.Error = err.Error()
        response, err := c.send(httpRequest, &record)
        if err != nil {
                record.Error = err.Error()
@@ -299,11 +321,20 @@ type ModelOutputItem struct {
        } `json:"content"`
 }
 
        } `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
 // 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
 type ModelRequest struct {
        Model        string
        Effort       string
@@ -312,6 +343,7 @@ type ModelRequest struct {
        Input        []any
        Node         *NodeKey
        Turn         string
        Input        []any
        Node         *NodeKey
        Turn         string
+       Agent        string
 }
 
 // ModelResponse is the result of a model call. Output holds the output items as
 }
 
 // ModelResponse is the result of a model call. Output holds the output items as
@@ -342,14 +374,16 @@ type ModelReasoning struct {
 // 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
 // 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"`
 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"`
        Status       int               `json:"status"`
        InputTokens  int               `json:"input_tokens"`
        CachedTokens int               `json:"cached_tokens"`
@@ -411,6 +445,7 @@ type ModelWireError struct {
 // without the turn state.
 func modelQuotaHeaders(header http.Header) map[string]string {
        var quota map[string]string
 // 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
        for name, values := range header {
                if !strings.HasPrefix(name, modelQuotaPrefix) || name == modelTurnStateHeader || len(values) == 0 {
                        continue
@@ -431,8 +466,11 @@ func modelQuotaHeaders(header http.Header) map[string]string {
 // the completed response when the stream sent none.
 func modelReadStream(stream io.Reader) (ModelResponse, error) {
        var response ModelResponse
 // the completed response when the stream sent none.
 func modelReadStream(stream io.Reader) (ModelResponse, error) {
        var response ModelResponse
+
        reader := bufio.NewReader(stream)
        reader := bufio.NewReader(stream)
+
        var data bytes.Buffer
        var data bytes.Buffer
+
        for {
                line, err := reader.ReadBytes('\n')
                if err != nil && !errors.Is(err, io.EOF) {
        for {
                line, err := reader.ReadBytes('\n')
                if err != nil && !errors.Is(err, io.EOF) {
@@ -449,6 +487,7 @@ func modelReadStream(stream io.Reader) (ModelResponse, error) {
                } else if data.Len() > 0 {
                        done, eventErr := modelReadEvent(&response, data.Bytes())
                        data.Reset()
                } else if data.Len() > 0 {
                        done, eventErr := modelReadEvent(&response, data.Bytes())
                        data.Reset()
+
                        if eventErr != nil || done {
                                return response, eventErr
                        }
                        if eventErr != nil || done {
                                return response, eventErr
                        }
@@ -464,6 +503,7 @@ func modelReadStream(stream io.Reader) (ModelResponse, error) {
 // stream.
 func modelReadEvent(response *ModelResponse, data []byte) (bool, error) {
        var event ModelStreamEvent
 // 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)
        err := json.Unmarshal(data, &event)
        if err != nil {
                return true, fmt.Errorf("decode stream event: %w", err)