Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions internal/providers/anthropic/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type Options struct {
// When unset, Zero uses providerio.ResolveStreamIdleTimeout — the
// ZERO_STREAM_IDLE_TIMEOUT override or providerio.DefaultStreamIdleTimeout.
StreamIdleTimeout time.Duration
SessionHeader string
}

// Provider streams completions from Anthropic's Messages API.
Expand All @@ -108,6 +109,7 @@ type Provider struct {
httpClient *http.Client
userAgent string
streamIdleTimeout time.Duration
sessionHeader providerio.SessionHeader
}

// New creates an Anthropic provider.
Expand Down Expand Up @@ -143,6 +145,7 @@ func New(options Options) (*Provider, error) {
httpClient: providerio.HTTPClient(options.HTTPClient),
userAgent: options.UserAgent,
streamIdleTimeout: providerio.ResolveStreamIdleTimeout(options.StreamIdleTimeout),
sessionHeader: providerio.NewSessionHeader(options.SessionHeader),
}, nil
}

Expand All @@ -163,12 +166,12 @@ func (provider *Provider) StreamCompletion(
events := make(chan zeroruntime.StreamEvent, 16)
go func() {
defer close(events)
provider.stream(ctx, body, events)
provider.stream(ctx, body, request.PromptCacheKey, events)
}()
return events, nil
}

func (provider *Provider) stream(ctx context.Context, body []byte, events chan<- zeroruntime.StreamEvent) {
func (provider *Provider) stream(ctx context.Context, body []byte, sessionID string, events chan<- zeroruntime.StreamEvent) {
// streamCtx lets the idle watchdog abort an in-flight body read by cancelling
// the request, which unblocks the SSE reader goroutine.
streamCtx, cancelStream := context.WithCancel(ctx)
Expand All @@ -185,6 +188,7 @@ func (provider *Provider) stream(ctx context.Context, body []byte, events chan<-
},
provider.oauthResolver,
func(request *http.Request) {
provider.sessionHeader.Apply(request, sessionID)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("anthropic-version", provider.version)
if provider.beta != "" {
Expand Down
20 changes: 20 additions & 0 deletions internal/providers/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/Gitlawb/zero/internal/config"
Expand Down Expand Up @@ -53,6 +54,11 @@ func New(profile config.ProviderProfile, options Options) (zeroruntime.Provider,
return newCodexProvider(profile, resolved, options)
}

sessionHeader := sessionHeaderForProfile(profile, resolved)
if sessionHeader != "" && strings.TrimSpace(options.UserAgent) == "" {
options.UserAgent = "zero"
}

switch resolved.providerKind {
case config.ProviderKindOpenAI, config.ProviderKindOpenAICompatible:
// prompt_cache_key is an OpenAI-only chat-completions field. Strict
Expand All @@ -74,6 +80,7 @@ func New(profile config.ProviderProfile, options Options) (zeroruntime.Provider,
UserAgent: options.UserAgent,
ParseThinkTags: parseThinkTagsForProfile(profile, resolved),
DisablePromptCacheKey: resolved.providerKind == config.ProviderKindOpenAICompatible,
SessionHeader: sessionHeader,
})
case config.ProviderKindAnthropic, config.ProviderKindAnthropicCompat:
return anthropic.New(anthropic.Options{
Expand All @@ -88,6 +95,7 @@ func New(profile config.ProviderProfile, options Options) (zeroruntime.Provider,
MaxTokens: resolved.maxOutputTokens,
HTTPClient: options.HTTPClient,
UserAgent: options.UserAgent,
SessionHeader: sessionHeader,
})
case config.ProviderKindGoogle:
return gemini.New(gemini.Options{
Expand Down Expand Up @@ -345,6 +353,18 @@ func isCodexCatalog(profile config.ProviderProfile, _ resolvedProfile) bool {
return providercatalog.NormalizeID(profile.CatalogID) == "chatgpt"
}

func sessionHeaderForProfile(profile config.ProviderProfile, resolved resolvedProfile) string {
switch providercatalog.NormalizeID(profile.CatalogID) {
case "opencode", "opencode-go", "opencode-go-anthropic-compatible":
return "x-opencode-session"
}
endpoint, err := url.Parse(resolved.baseURL)
if err == nil && strings.EqualFold(endpoint.Hostname(), "opencode.ai") && (endpoint.Path == "/zen" || strings.HasPrefix(endpoint.Path, "/zen/")) {
return "x-opencode-session"
}
return ""
}

// newCodexProvider builds a Codex-flavored openai provider for the chatgpt
// catalog. The Codex headers (`originator`, `chatgpt-account-id`) are
// injected by the CodexProvider wrapper. The `chatgpt-account-id` is read from
Expand Down
Loading
Loading