Skip to content

Commit 300acdc

Browse files
authored
Refactor ApiProxyOptions into composed, domain-focused option interfaces (#5060)
* Initial plan * refactor: split ApiProxyOptions into focused sub-interfaces --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent eed40e4 commit 300acdc

6 files changed

Lines changed: 605 additions & 552 deletions
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* API proxy enablement, credentials, and auth customization options.
3+
*/
4+
5+
export interface ApiProxyCredentialOptions {
6+
/**
7+
* Enable API proxy sidecar for holding authentication credentials
8+
*
9+
* When true, deploys a Node.js proxy sidecar container that:
10+
* - Holds OpenAI, Anthropic, GitHub Copilot, and Google Gemini API keys securely
11+
* - Automatically injects authentication headers
12+
* - Routes all traffic through Squid to respect domain whitelisting
13+
* - Proxies requests to LLM providers
14+
*
15+
* The sidecar exposes four endpoints accessible from the agent container:
16+
* - http://api-proxy:10000 - OpenAI API proxy (for Codex) {@link API_PROXY_PORTS.OPENAI}
17+
* - http://api-proxy:10001 - Anthropic API proxy (for Claude) {@link API_PROXY_PORTS.ANTHROPIC}
18+
* - http://api-proxy:10002 - GitHub Copilot API proxy {@link API_PROXY_PORTS.COPILOT}
19+
* - http://api-proxy:10003 - Google Gemini API proxy {@link API_PROXY_PORTS.GEMINI}
20+
*
21+
* When the corresponding API key is provided, the following environment
22+
* variables are set in the agent container:
23+
* - OPENAI_BASE_URL=http://api-proxy:10000 (set when OPENAI_API_KEY is provided)
24+
* - ANTHROPIC_BASE_URL=http://api-proxy:10001 (set when ANTHROPIC_API_KEY is provided, or when AWF_AUTH_TYPE=github-oidc and AWF_AUTH_PROVIDER=anthropic)
25+
* - COPILOT_API_URL=http://api-proxy:10002 (set when COPILOT_GITHUB_TOKEN is provided)
26+
* - CLAUDE_CODE_API_KEY_HELPER=/usr/local/bin/get-claude-key.sh (set when ANTHROPIC_API_KEY is provided, or when AWF_AUTH_TYPE=github-oidc and AWF_AUTH_PROVIDER=anthropic)
27+
*
28+
* API keys are passed via environment variables:
29+
* - OPENAI_API_KEY - Optional OpenAI API key for Codex
30+
* - ANTHROPIC_API_KEY - Optional Anthropic API key for Claude
31+
* - COPILOT_GITHUB_TOKEN - Optional GitHub token for Copilot
32+
* - COPILOT_PROVIDER_API_KEY - Optional upstream BYOK API key for Copilot-compatible providers
33+
* - GEMINI_API_KEY - Optional Google Gemini API key
34+
*
35+
* @default false
36+
* @example
37+
* ```bash
38+
* # Enable API proxy with keys from environment
39+
* export OPENAI_API_KEY="sk-..."
40+
* export ANTHROPIC_API_KEY="sk-ant-..."
41+
* export COPILOT_GITHUB_TOKEN="ghp_..."
42+
* awf --enable-api-proxy --allow-domains api.openai.com,api.anthropic.com,api.githubcopilot.com -- command
43+
* ```
44+
* @see API_PROXY_PORTS for port configuration
45+
*/
46+
enableApiProxy?: boolean;
47+
48+
/**
49+
* OpenAI API key for Codex (used by API proxy sidecar)
50+
*
51+
* When enableApiProxy is true, this key is injected into the Node.js sidecar
52+
* container and used to authenticate requests to api.openai.com.
53+
*
54+
* The key is NOT exposed to the agent container - only the proxy URL is provided.
55+
*
56+
* @default undefined
57+
*/
58+
openaiApiKey?: string;
59+
60+
/**
61+
* Anthropic API key for Claude (used by API proxy sidecar)
62+
*
63+
* When enableApiProxy is true, this key is injected into the Node.js sidecar
64+
* container and used to authenticate requests to api.anthropic.com.
65+
*
66+
* The key is NOT exposed to the agent container - only the proxy URL is provided.
67+
*
68+
* @default undefined
69+
*/
70+
anthropicApiKey?: string;
71+
72+
/**
73+
* GitHub token for Copilot (used by API proxy sidecar)
74+
*
75+
* When enableApiProxy is true, this token is injected into the Node.js sidecar
76+
* container and used to authenticate requests to api.githubcopilot.com.
77+
*
78+
* The token is NOT exposed to the agent container - only the proxy URL is provided.
79+
* The agent receives a placeholder value that is protected by the one-shot-token library.
80+
*
81+
* @default undefined
82+
*/
83+
copilotGithubToken?: string;
84+
85+
/**
86+
* Upstream BYOK API key for Copilot-compatible providers (used by API proxy sidecar)
87+
*
88+
* When enableApiProxy is true and this key is provided, AWF routes Copilot CLI
89+
* through the sidecar in direct-BYOK mode (Azure Foundry, OpenRouter, etc.).
90+
* The real key is injected into the Node.js sidecar container and used to
91+
* authenticate requests to the user-supplied COPILOT_PROVIDER_BASE_URL.
92+
*
93+
* The key is NOT exposed to the agent container - only the proxy URL is provided.
94+
* The agent receives a placeholder value so Copilot CLI's startup auth check passes.
95+
*
96+
* Sourced from `process.env.COPILOT_PROVIDER_API_KEY` in build-config; matches the
97+
* pattern used by OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_GITHUB_TOKEN, and
98+
* GEMINI_API_KEY.
99+
*
100+
* @default undefined
101+
*/
102+
copilotProviderApiKey?: string;
103+
104+
/**
105+
* Google Gemini API key (used by API proxy sidecar)
106+
*
107+
* When enableApiProxy is true, this key is injected into the Node.js sidecar
108+
* container and used to authenticate requests to generativelanguage.googleapis.com.
109+
*
110+
* The key is NOT exposed to the agent container - only the proxy URL is provided.
111+
* The agent receives a placeholder value so Gemini CLI's startup auth check passes.
112+
*
113+
* @default undefined
114+
*/
115+
geminiApiKey?: string;
116+
117+
/**
118+
* Custom auth header name for OpenAI API requests (used by API proxy sidecar)
119+
*
120+
* When set, the proxy uses this header name instead of the default
121+
* standard Authorization bearer-header format. The key is sent as the raw header
122+
* value without a "Bearer" prefix.
123+
*
124+
* Useful for internal AI gateways (e.g. Azure OpenAI) that require a
125+
* different header name such as `api-key`.
126+
*
127+
* Can be set via:
128+
* - CLI flag: `--openai-api-auth-header <name>`
129+
* - Environment variable: `AWF_OPENAI_AUTH_HEADER`
130+
*
131+
* @default undefined (uses a standard Authorization bearer header)
132+
* @example 'api-key'
133+
*/
134+
openaiApiAuthHeader?: string;
135+
136+
/**
137+
* Custom auth header name for Anthropic API requests (used by API proxy sidecar)
138+
*
139+
* When set, the proxy uses this header name instead of the default `x-api-key`.
140+
*
141+
* Useful for internal AI gateways that require a different header name.
142+
*
143+
* Can be set via:
144+
* - CLI flag: `--anthropic-api-auth-header <name>`
145+
* - Environment variable: `AWF_ANTHROPIC_AUTH_HEADER`
146+
*
147+
* @default 'x-api-key'
148+
* @example 'api-key'
149+
*/
150+
anthropicApiAuthHeader?: string;
151+
152+
/**
153+
* Anthropic OIDC token exchange endpoint override.
154+
*
155+
* When set, AWF passes this value to the API proxy as
156+
* `AWF_AUTH_ANTHROPIC_TOKEN_URL` for Anthropic WIF/OIDC exchange.
157+
*
158+
* Intended for non-sensitive endpoint customization and typically set via
159+
* config file (`apiProxy.auth.anthropicTokenUrl`).
160+
*
161+
* @default 'https://api.anthropic.com/v1/oauth/token'
162+
*/
163+
anthropicTokenUrl?: string;
164+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* API proxy diagnostics, logging, and cache options.
3+
*/
4+
5+
export interface ApiProxyDiagnosticsOptions {
6+
/**
7+
* Enable detailed token and model-alias diagnostic logging.
8+
*
9+
* When true, the API proxy writes diagnostic events to `token-diag.jsonl`
10+
* including:
11+
* - `MODEL_ALIAS_RESOLUTION_STEP` — each step of the alias resolution chain
12+
* - `MODEL_ALIAS_REWRITE` — final alias rewrite decision
13+
* - Token usage summaries and per-request diagnostics
14+
*
15+
* The `token-diag.jsonl` file is written alongside the `token-usage.jsonl`
16+
* in the directory specified by `tokenLogDir`.
17+
*
18+
* Set via:
19+
* - Config file: `apiProxy.logging.debugTokens: true`
20+
* - Environment variable: `AWF_DEBUG_TOKENS=1`
21+
*
22+
* @default false
23+
*/
24+
debugTokens?: boolean;
25+
26+
/**
27+
* Directory path for API proxy log files (`token-usage.jsonl` and
28+
* `token-diag.jsonl`). In the default AWF compose, this must be `/var/log/api-proxy`
29+
* (or a subdirectory) so logs are written to the mounted volume.
30+
*
31+
* Set via:
32+
* - Config file: `apiProxy.logging.tokenLogDir: "/var/log/api-proxy/custom"`
33+
* - Environment variable: `AWF_TOKEN_LOG_DIR=/var/log/api-proxy/custom`
34+
*
35+
* @default "/var/log/api-proxy"
36+
*/
37+
tokenLogDir?: string;
38+
39+
/**
40+
* Enable capture of body-shape diagnostics for guard-blocked requests.
41+
*
42+
* - `false` (default): Nothing written.
43+
* - `'summary'`: Counts, sizes, hashes — no message content.
44+
* - `'redacted'`: Summary plus first 200 chars per message.
45+
* - `'full'`: Full body up to `maxCapturedBytes`.
46+
* - `true`: Alias for `'summary'`.
47+
*
48+
* Set via:
49+
* - Config file: `apiProxy.diagnostics.captureBlockedRequests: "summary"`
50+
* - Environment variable: `AWF_CAPTURE_BLOCKED_LLM_REQUESTS=summary`
51+
*
52+
* @default false
53+
*/
54+
captureBlockedRequests?: boolean | 'summary' | 'redacted' | 'full';
55+
56+
/**
57+
* Maximum body bytes to include in a single `full`-mode blocked-request-diag record.
58+
*
59+
* Set via:
60+
* - Config file: `apiProxy.diagnostics.maxCapturedBytes: 250000`
61+
* - Environment variable: `AWF_MAX_BLOCKED_CAPTURE_BYTES=250000`
62+
*
63+
* @default 250000
64+
*/
65+
maxCapturedBytes?: number;
66+
67+
/**
68+
* Enable Anthropic prompt-cache optimizations in the API proxy sidecar.
69+
*
70+
* When true, the Anthropic proxy (port 10001) automatically mutates every
71+
* POST /v1/messages request before forwarding it to api.anthropic.com:
72+
*
73+
* - Injects prompt-cache breakpoints on tools, system, messages[0], and the
74+
* rolling tail where they are missing — reducing the uncached token count
75+
* for repetitive content to near zero.
76+
* - Upgrades existing ephemeral cache TTLs from the implicit 5-minute default
77+
* to 1 hour on stable content (tools, system, messages[0]); the rolling tail
78+
* stays at the shorter TTL configured by `anthropicCacheTailTtl`.
79+
* - Adds the `anthropic-beta: extended-cache-ttl-2025-04-11` header required
80+
* by the Anthropic API to honour 1h TTLs.
81+
* - Strips ANSI SGR escape sequences from message text and tool results so
82+
* terminal output with colour codes caches cleanly.
83+
*
84+
* Requires `enableApiProxy: true`. Has no effect without an `ANTHROPIC_API_KEY`.
85+
*
86+
* Set via:
87+
* - CLI flag: `--anthropic-auto-cache`
88+
* - Config file: `apiProxy.anthropicAutoCache: true`
89+
*
90+
* @default false
91+
*/
92+
anthropicAutoCache?: boolean;
93+
94+
/**
95+
* TTL for the rolling-tail cache breakpoint when `anthropicAutoCache` is enabled.
96+
*
97+
* The rolling tail is the last cacheable block across all messages; it moves every
98+
* turn so a shorter TTL is more cost-effective than 1h (avoids paying the 2.0×
99+
* write multiplier for a breakpoint that will expire before reuse).
100+
*
101+
* - `"5m"` (default): 5-minute TTL. Suitable for interactive sessions with
102+
* fast back-and-forth turns.
103+
* - `"1h"`: 1-hour TTL. Better for long-running agentic tasks where individual
104+
* turns may take minutes.
105+
*
106+
* Only used when `anthropicAutoCache` is true.
107+
*
108+
* Set via:
109+
* - CLI flag: `--anthropic-cache-tail-ttl <5m|1h>`
110+
* - Config file: `apiProxy.anthropicCacheTailTtl: "1h"`
111+
*
112+
* @default "5m"
113+
*/
114+
anthropicCacheTailTtl?: '5m' | '1h';
115+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* API proxy model routing and selection options.
3+
*/
4+
5+
export interface ApiProxyModelOptions {
6+
/**
7+
* Model fallback policy for unresolved model selections in the API proxy.
8+
*
9+
* When enabled, if direct model selection and alias resolution both fail,
10+
* the proxy selects a "middle-power" model (median by capability tier) from
11+
* available provider models as a safety fallback.
12+
*
13+
* @default { enabled: true, strategy: 'middle_power' }
14+
*/
15+
modelFallback?: {
16+
enabled?: boolean;
17+
strategy?: 'middle_power';
18+
excludeEngines?: string[];
19+
};
20+
21+
/**
22+
* Model alias map for the API proxy sidecar
23+
*
24+
* When enableApiProxy is true and model aliases are configured, the proxy
25+
* intercepts POST/PUT/PATCH request bodies containing a "model" field and rewrites
26+
* the model name using the alias resolution chain before forwarding to upstream.
27+
*
28+
* Alias map format: each key is an alias name (or "" for the default policy),
29+
* and the value is an ordered list of candidates. Candidates can be:
30+
* - "provider/modelpattern" — match against available models for that provider
31+
* using case-insensitive glob patterns (* wildcard)
32+
* - "alias-name" — recursively expand another alias (loop detection applies)
33+
*
34+
* Resolution picks the highest-version matching model (semver semantics).
35+
* Only models for the receiving provider's port are considered (e.g., the
36+
* Copilot proxy at port 10002 only matches "copilot/*" patterns).
37+
*
38+
* Set via the `apiProxy.models` section of the AWF config file.
39+
*
40+
* @example
41+
* ```json
42+
* {
43+
* "sonnet": ["copilot/*sonnet*", "anthropic/*sonnet*"],
44+
* "gpt-5-codex": ["copilot/gpt-5*-codex", "openai/gpt-5*-codex"],
45+
* "": ["sonnet", "gpt-5-codex"]
46+
* }
47+
* ```
48+
*/
49+
modelAliases?: Record<string, string[]>;
50+
51+
/**
52+
* Expected model name for pre-startup validation.
53+
*
54+
* When set, the API proxy validates at startup that this model is available
55+
* in at least one configured provider's model catalogue. If the model is not
56+
* found (retired, restricted, or misspelled), a clear `model_unavailable_at_startup`
57+
* diagnostic is emitted. This does not block proxy startup.
58+
*
59+
* - Config: `apiProxy.requestedModel`
60+
* - Environment variable: `AWF_REQUESTED_MODEL` (internal; set by AWF CLI)
61+
*
62+
* @example 'gpt-4o'
63+
*/
64+
requestedModel?: string;
65+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { ApiProxyOptions } from './api-proxy-options';
2+
3+
describe('ApiProxyOptions', () => {
4+
it('composes fields from credential, routing, model, and diagnostics options', () => {
5+
const options: ApiProxyOptions = {
6+
enableApiProxy: true,
7+
openaiApiKey: 'test-key',
8+
openaiApiTarget: 'api.openai.com',
9+
modelAliases: { default: ['openai/*'] },
10+
debugTokens: true,
11+
};
12+
13+
expect(options.enableApiProxy).toBe(true);
14+
expect(options.openaiApiTarget).toBe('api.openai.com');
15+
expect(options.modelAliases).toEqual({ default: ['openai/*'] });
16+
expect(options.debugTokens).toBe(true);
17+
});
18+
});

0 commit comments

Comments
 (0)