Skip to content
Open
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
1 change: 1 addition & 0 deletions pkg/api/harness_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type HarnessAuthCapabilities struct {
AuthFile CapabilityField `json:"auth_file" yaml:"auth_file"`
OAuthToken CapabilityField `json:"oauth_token" yaml:"oauth_token"`
VertexAI CapabilityField `json:"vertex_ai" yaml:"vertex_ai"`
LLMGateway CapabilityField `json:"llm_gateway" yaml:"llm_gateway"`
}

// HarnessMCPCapabilities describes MCP transport support for a harness.
Expand Down
8 changes: 5 additions & 3 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,9 +475,11 @@ type AuthConfig struct {
OAuthCreds string

// Anthropic auth
AnthropicAPIKey string
ClaudeOAuthToken string // CLAUDE_CODE_OAUTH_TOKEN (long-lived, from `claude setup-token`)
ClaudeAuthFile string // ~/.claude/.credentials.json path (rotating refresh-token store)
AnthropicAPIKey string
AnthropicAuthToken string // ANTHROPIC_AUTH_TOKEN (alias used by LLM proxies)
AnthropicBaseURL string // ANTHROPIC_BASE_URL (custom API endpoint, e.g. LiteLLM proxy)
ClaudeOAuthToken string // CLAUDE_CODE_OAUTH_TOKEN (long-lived, from `claude setup-token`)
ClaudeAuthFile string // ~/.claude/.credentials.json path (rotating refresh-token store)

// OpenAI/Codex auth
OpenAIAPIKey string
Expand Down
15 changes: 8 additions & 7 deletions pkg/config/schemas/settings-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@
},
"auth_selected_type": {
"type": "string",
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai"],
"description": "Authentication mechanism to use (e.g., api-key, oauth-token, vertex-ai, auth-file)."
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai", "llm-gateway"],
"description": "Authentication mechanism to use (e.g., api-key, oauth-token, vertex-ai, auth-file, llm-gateway)."
},
"secrets": {
"type": "array",
Expand Down Expand Up @@ -453,7 +453,8 @@
"api_key": { "$ref": "#/$defs/capabilityField" },
"auth_file": { "$ref": "#/$defs/capabilityField" },
"oauth_token": { "$ref": "#/$defs/capabilityField" },
"vertex_ai": { "$ref": "#/$defs/capabilityField" }
"vertex_ai": { "$ref": "#/$defs/capabilityField" },
"llm_gateway": { "$ref": "#/$defs/capabilityField" }
},
"additionalProperties": false
},
Expand All @@ -475,7 +476,7 @@
"properties": {
"default_type": {
"type": "string",
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai"]
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai", "llm-gateway"]
},
"types": {
"type": "object",
Expand Down Expand Up @@ -533,14 +534,14 @@
"type": "object",
"additionalProperties": {
"type": "string",
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai"]
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai", "llm-gateway"]
}
},
"files": {
"type": "object",
"additionalProperties": {
"type": "string",
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai"]
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai", "llm-gateway"]
}
}
},
Expand Down Expand Up @@ -604,7 +605,7 @@
"resources": { "$ref": "#/$defs/resourceSpec" },
"auth_selected_type": {
"type": "string",
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai"]
"enum": ["api-key", "oauth-token", "auth-file", "vertex-ai", "llm-gateway"]
}
},
"additionalProperties": false
Expand Down
19 changes: 13 additions & 6 deletions pkg/harness/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ func GatherAuthWithEnv(env map[string]string, localSources bool) api.AuthConfig

auth := api.AuthConfig{
// Env-var sourced fields
GeminiAPIKey: lookup("GEMINI_API_KEY"),
GoogleAPIKey: lookup("GOOGLE_API_KEY"),
AnthropicAPIKey: lookup("ANTHROPIC_API_KEY"),
ClaudeOAuthToken: lookup("CLAUDE_CODE_OAUTH_TOKEN"),
OpenAIAPIKey: lookup("OPENAI_API_KEY"),
CodexAPIKey: lookup("CODEX_API_KEY"),
GeminiAPIKey: lookup("GEMINI_API_KEY"),
GoogleAPIKey: lookup("GOOGLE_API_KEY"),
AnthropicAPIKey: lookup("ANTHROPIC_API_KEY"),
AnthropicAuthToken: lookup("ANTHROPIC_AUTH_TOKEN"),
AnthropicBaseURL: lookup("ANTHROPIC_BASE_URL"),
ClaudeOAuthToken: lookup("CLAUDE_CODE_OAUTH_TOKEN"),
OpenAIAPIKey: lookup("OPENAI_API_KEY"),
CodexAPIKey: lookup("CODEX_API_KEY"),
GoogleCloudProject: util.FirstNonEmpty(
lookup("GOOGLE_CLOUD_PROJECT"),
lookup("GCP_PROJECT"),
Expand Down Expand Up @@ -292,6 +294,9 @@ func DetectAuthTypeFromEnvVars(harnessName string, envKeys map[string]struct{})

switch harnessName {
case "claude":
if _, ok := envKeys["ANTHROPIC_BASE_URL"]; ok {
return "llm-gateway"
}
if _, ok := envKeys["ANTHROPIC_API_KEY"]; ok {
return ""
}
Expand Down Expand Up @@ -355,6 +360,8 @@ func RequiredAuthEnvKeys(harnessName, authSelectedType string) [][]string {
return nil
case "vertex-ai":
return [][]string{{"GOOGLE_CLOUD_PROJECT"}, {"GOOGLE_CLOUD_REGION", "CLOUD_ML_REGION", "GOOGLE_CLOUD_LOCATION"}}
case "llm-gateway":
return [][]string{{"ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"}, {"ANTHROPIC_BASE_URL"}}
}
case "gemini":
switch effectiveType {
Expand Down
6 changes: 6 additions & 0 deletions pkg/harness/claude/embeds/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ capabilities:
auth_file: { support: "yes" }
oauth_token: { support: "yes" }
vertex_ai: { support: "yes" }
llm_gateway: { support: "yes" }
auth:
default_type: api-key
types:
Expand All @@ -73,8 +74,13 @@ auth:
alternative_env_keys: ["GOOGLE_APPLICATION_CREDENTIALS"]
skipped_when_gcp_service_account_assigned: true
required: true
llm-gateway:
required_env:
- any_of: ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"]
- any_of: ["ANTHROPIC_BASE_URL"]
autodetect:
env:
ANTHROPIC_BASE_URL: llm-gateway
CLAUDE_CODE_OAUTH_TOKEN: oauth-token
GOOGLE_APPLICATION_CREDENTIALS: vertex-ai
GOOGLE_CLOUD_PROJECT: vertex-ai
Expand Down
57 changes: 52 additions & 5 deletions pkg/harness/claude_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (c *ClaudeCode) AdvancedCapabilities() api.HarnessAdvancedCapabilities {
AuthFile: api.CapabilityField{Support: api.SupportYes},
OAuthToken: api.CapabilityField{Support: api.SupportYes},
VertexAI: api.CapabilityField{Support: api.SupportYes},
LLMGateway: api.CapabilityField{Support: api.SupportYes},
},
Resume: api.CapabilityField{Support: api.SupportYes},
}
Expand Down Expand Up @@ -139,6 +140,11 @@ func (c *ClaudeCode) Provision(ctx context.Context, agentName, agentDir, agentHo
"ANTHROPIC_VERTEX_PROJECT_ID": "${GOOGLE_CLOUD_PROJECT}",
"CLOUD_ML_REGION": "${GOOGLE_CLOUD_REGION}",
}
case "llm-gateway":
envUpdates = map[string]string{
"ANTHROPIC_AUTH_TOKEN": "${ANTHROPIC_AUTH_TOKEN}",
"ANTHROPIC_BASE_URL": "${ANTHROPIC_BASE_URL}",
}
}

if len(envUpdates) > 0 {
Expand Down Expand Up @@ -242,10 +248,15 @@ func (c *ClaudeCode) provisionClaudeJSON(ctx context.Context, agentHome, agentWo
// api-key auth. This pre-approves the API key so Claude Code does not prompt
// for confirmation.
func (c *ClaudeCode) ApplyAuthSettings(agentHome string, resolved *api.ResolvedAuth) error {
if resolved.Method != "api-key" {
var apiKey string
switch resolved.Method {
case "api-key":
apiKey = resolved.EnvVars["ANTHROPIC_API_KEY"]
case "llm-gateway":
apiKey = resolved.EnvVars["ANTHROPIC_AUTH_TOKEN"]
default:
return nil
}
apiKey := resolved.EnvVars["ANTHROPIC_API_KEY"]
if apiKey == "" {
return nil
}
Expand Down Expand Up @@ -376,12 +387,48 @@ func (c *ClaudeCode) ResolveAuth(auth api.AuthConfig) (*api.ResolvedAuth, error)
return nil, fmt.Errorf("claude: auth type %q selected but GOOGLE_CLOUD_PROJECT and/or GOOGLE_CLOUD_REGION not set", auth.SelectedType)
}
return c.resolveVertexAI(auth), nil
case "llm-gateway":
token := auth.AnthropicAuthToken
if token == "" {
token = auth.AnthropicAPIKey
}
if token == "" {
return nil, fmt.Errorf("claude: auth type %q selected but no token found; set ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY", auth.SelectedType)
}
if auth.AnthropicBaseURL == "" {
return nil, fmt.Errorf("claude: auth type %q selected but ANTHROPIC_BASE_URL is not set", auth.SelectedType)
}
return &api.ResolvedAuth{
Method: "llm-gateway",
EnvVars: map[string]string{
"ANTHROPIC_AUTH_TOKEN": token,
"ANTHROPIC_BASE_URL": auth.AnthropicBaseURL,
},
}, nil
default:
return nil, fmt.Errorf("claude: unknown auth type %q; valid types are: api-key, oauth-token, auth-file, vertex-ai", auth.SelectedType)
return nil, fmt.Errorf("claude: unknown auth type %q; valid types are: api-key, oauth-token, auth-file, vertex-ai, llm-gateway", auth.SelectedType)
}
}

// Auto-detect preference order: API key → OAuth token → credentials file → Vertex AI → error
// Auto-detect preference order: LLM Gateway → API key → OAuth token → credentials file → Vertex AI → error

// 0. LLM proxy (ANTHROPIC_BASE_URL + (ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY))
if auth.AnthropicBaseURL != "" {
token := auth.AnthropicAuthToken
if token == "" {
token = auth.AnthropicAPIKey
}
if token == "" {
return nil, fmt.Errorf("claude: ANTHROPIC_BASE_URL is set but no token found; set ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY")
}
return &api.ResolvedAuth{
Method: "llm-gateway",
EnvVars: map[string]string{
"ANTHROPIC_AUTH_TOKEN": token,
"ANTHROPIC_BASE_URL": auth.AnthropicBaseURL,
},
}, nil
}

// 1. Anthropic API key (direct)
if auth.AnthropicAPIKey != "" {
Expand Down Expand Up @@ -424,7 +471,7 @@ func (c *ClaudeCode) ResolveAuth(auth api.AuthConfig) (*api.ResolvedAuth, error)
return c.resolveVertexAI(auth), nil
}

return nil, fmt.Errorf("claude: no valid auth method found; set ANTHROPIC_API_KEY for direct API access, CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`) or ~/.claude/.credentials.json for subscription auth, or provide ADC (gcloud-adc secret, GCP service account, or ~/.config/gcloud/application_default_credentials.json) + GOOGLE_CLOUD_PROJECT + GOOGLE_CLOUD_REGION for Vertex AI")
return nil, fmt.Errorf("claude: no valid auth method found; set ANTHROPIC_API_KEY for direct API access, ANTHROPIC_AUTH_TOKEN + ANTHROPIC_BASE_URL for LLM proxy (e.g. LiteLLM), CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`) or ~/.claude/.credentials.json for subscription auth, or provide ADC (gcloud-adc secret, GCP service account, or ~/.config/gcloud/application_default_credentials.json) + GOOGLE_CLOUD_PROJECT + GOOGLE_CLOUD_REGION for Vertex AI")
}

func (c *ClaudeCode) resolveVertexAI(auth api.AuthConfig) *api.ResolvedAuth {
Expand Down
2 changes: 2 additions & 0 deletions pkg/harness/container_script_harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ func (c *ContainerScriptHarness) ResolveAuth(auth api.AuthConfig) (*api.Resolved
}
}
addIfPresent("ANTHROPIC_API_KEY", auth.AnthropicAPIKey)
addIfPresent("ANTHROPIC_AUTH_TOKEN", auth.AnthropicAuthToken)
addIfPresent("ANTHROPIC_BASE_URL", auth.AnthropicBaseURL)
addIfPresent("CLAUDE_CODE_OAUTH_TOKEN", auth.ClaudeOAuthToken)
addIfPresent("OPENAI_API_KEY", auth.OpenAIAPIKey)
addIfPresent("GEMINI_API_KEY", auth.GeminiAPIKey)
Expand Down