feat(translation): API translation proxy — enable Claude integration cross-platform#152
Conversation
… proxy 133 tests covering: - Canonical IR type-level checks (discriminated unions, all variants) - AnthropicCodec: parseRequest (18 cases), serializeResponse (7), stream serializer (8) - OpenAICodec: serializeRequest (15 cases), parseResponse (11), stream parser state machine (10) - ToolNameMappingMiddleware: truncation/reverse-map/uniqueness (9 cases) - PromptCacheMiddleware: cache hit/miss detection, no-double-count (5 cases) - LoggingMiddleware: metadata-only, no key/body leakage (6 cases) - LineBuffer: CRLF, partial lines, flush (11 cases) - StreamTranslator: end-to-end SSE translation (9 cases) - TranslationProxy: HTTP server round trips, error mapping, 413, streaming (10 cases) Co-Authored-By: Claude <noreply@anthropic.com>
Adds the foundational translation layer for proxying Anthropic Messages API requests to OpenAI Chat Completions API (and back) transparently. Components: - src/translation/ir.ts: Canonical IR types (lingua franca between codecs) - src/translation/codec.ts: FormatCodec/StreamParser/StreamSerializer interfaces - src/translation/codecs/anthropic-codec.ts: Source codec (Claude Code wire format) - src/translation/codecs/openai-codec.ts: Target codec with SSE stream parser state machine - src/translation/middleware/middleware.ts: Pipeline runner (request fwd, response/stream reverse) - src/translation/middleware/tool-name-mapping.ts: 64-char limit truncation + SHA256 uniqueness - src/translation/middleware/prompt-cache.ts: Metrics-only prefix-hash cache tracking - src/translation/middleware/logging.ts: Structured metadata logging, never logs API keys - src/translation/proxy/line-buffer.ts: SSE chunk boundary assembler (CRLF-aware) - src/translation/proxy/stream-translator.ts: OpenAI→Anthropic SSE event pipeline - src/translation/proxy/translation-proxy.ts: HTTP server, error mapping, 50MB limit, timeouts Security invariants enforced: - NEVER forwards inbound x-api-key; uses config.targetApiKey only - Strips ALL anthropic-* headers before forwarding - Never includes API keys in error messages or logs - Local server uses HTTP (loopback-only, intentional; outbound uses HTTPS) Snyk finding: CWE-319 Medium — intentional ARCHITECTURE EXCEPTION for loopback server Co-Authored-By: Claude <noreply@anthropic.com>
…lation proxy Adds ProxyManager service and ProxiedClaudeAdapter that enable Claude Code to route Anthropic Messages API requests through a local translation proxy to OpenAI-compatible backends. - src/translation/proxy/proxy-manager.ts: ProxyManager lifecycle service; loadProxyConfig() reads agents.claude.proxy config; non-fatal startup failure falls back to direct API - src/translation/proxy/proxied-claude-adapter.ts: ClaudeAdapter subclass that overrides resolveBaseUrl() to inject proxy URL (ANTHROPIC_BASE_URL=http://127.0.0.1:<port>) - src/bootstrap.ts: optional proxy startup before agentRegistry registration; registers ProxiedClaudeAdapter when proxy config exists; skipped when processSpawner is injected (tests) - tests: 3 new test files covering ProxyManager lifecycle, ProxiedClaudeAdapter spawn env, and loadProxyConfig config-file parsing (23 new tests, 156 total in translation suite)
…Adapter tests Dynamic import() in beforeEach returned a different module instance than the vi.mock() declaration, causing mockSpawn.mock.calls to be empty in test assertions. Fix: use static top-level import after vi.mock() calls, matching the established pattern from agent-adapters.test.ts.
…tibility Tests exposed resolveBaseUrl via test subclass instead of mocking child_process.spawn — prior test files in the same vitest fork loaded child_process unmocked, making vi.mock ineffective with isolate: false.
…ration When `translate: 'openai'` is set on an agent, the existing baseUrl, apiKey, and model fields become the target backend config for the translation proxy. This keeps configuration simple — users set 4 fields via the same `beat agents config set` / `ConfigureAgent` flow. Changes: - AgentConfig: add translate? field; saveAgentConfig key union extended - loadProxyConfig: reads translate + baseUrl/apiKey/model from AgentConfig - MCP: ConfigureAgent accepts translate in set/check; ListAgents shows it - CLI: agents config set/show support translate key - MCP instructions: document API translation feature and setup steps - Tests: updated bootstrap-proxy-integration + proxied-claude-adapter tests
- Remove identity function serializeStopReason from AnthropicCodec - Remove unused messageId/messageModel fields from AnthropicStreamSerializer - Tighten OpenAIMessage.content type from unknown to string|array|null - Remove unused contentType and processedRequest vars from TranslationProxy - Collapse redundant dual-condition in PromptCacheMiddleware.hashPrefix
…ning stream, shutdown - Add config validation warnings when translate is set without baseUrl/apiKey/model (MCP ConfigureAgent + CLI agents config set) - Handle streaming reasoning_content in OpenAIStreamParser → thinking_delta events - Add document content type fallback in OpenAI codec (converted to text placeholder) - Add proxyManager.stop() to MCP server graceful shutdown handler
Confidence Score: 5/5Safe to merge — all known P1 issues have been resolved; remaining findings are observability gaps and an edge-case flush omission No P0 or P1 findings remain. The three P2 items are: (1) streaming abort returns 502 instead of 499, (2) LoggingMiddleware response log never fires on the streaming path, and (3) flush() doesn't close active tool calls on abnormal stream termination. None affect correctness on the happy path. src/translation/codecs/openai-codec.ts (flush gap), src/translation/middleware/logging.ts (streaming observability), src/translation/proxy/translation-proxy.ts (abort code inconsistency) Important Files Changed
Sequence DiagramsequenceDiagram
participant CC as Claude Code
participant PA as ProxiedClaudeAdapter
participant TP as TranslationProxy (127.0.0.1:PORT)
participant MW as Middleware Pipeline
participant ST as StreamTranslator
participant OB as OpenAI Backend
CC->>PA: spawn with ANTHROPIC_BASE_URL
CC->>TP: POST /v1/messages (Anthropic format)
TP->>TP: AnthropicCodec.parseRequest
TP->>MW: runRequestMiddleware
MW-->>TP: processed CanonicalRequest
TP->>TP: OpenAICodec.serializeRequest
alt Streaming
TP->>OB: POST /chat/completions stream=true
OB-->>TP: SSE chunks
loop per SSE line
TP->>ST: processLine
ST->>MW: applyMiddleware processStreamEvent
ST-->>CC: Anthropic SSE event
end
TP->>ST: flush()
else Non-streaming
TP->>OB: POST /chat/completions
OB-->>TP: JSON response
TP->>MW: runResponseMiddleware
TP-->>CC: 200 Anthropic JSON
end
Reviews (7): Last reviewed commit: "refactor: simplify resolver fixes — remo..." | Re-trigger Greptile |
Code Review Summary: 17 Blocking Issues FoundCreated: 2026-04-23 | Branch: feat/api-translation-proxy → main Critical Issues by CategoryArchitecture (3 HIGH - Stateful Middleware Concurrency)
Complexity (4 HIGH - Function Length + Cyclomatic Complexity)
TypeScript (4 MEDIUM - Type Safety)
Security (2 MEDIUM - Input Validation)
Performance (2 MEDIUM - Hot Path Inefficiencies)
Testing (2 HIGH)
Regression (1 HIGH - Lifecycle)
Consistency (1 MEDIUM)
Issues by Confidence Level≥80% Confidence (17 BLOCKING) 60-79% Confidence (Lower Priority) Detailed Files
RecommendationStatus: CHANGES_REQUESTED The translation proxy feature demonstrates strong architectural fundamentals but has three critical concurrency bugs (stateful middleware shared across requests) that must be fixed before merge. Additionally, function decomposition (complexity), test coverage (middleware runner, error paths), and type safety improvements should be addressed. Next Steps:
Generated by Claude Code Code Review Agent |
COMP-6: convert parseContentBlock if-chain to switch statement for
clarity and uniform branch structure across 7 content types.
TS-4: add exhaustive never check in AnthropicStreamSerializer.serialize()
default branch so the compiler flags missing cases when new
CanonicalStreamEvent discriminants are added.
CON-1: fix logging middleware import path from '../../translation/ir.js'
to '../ir.js' — consistent with every other file in the same directory.
CON-2: replace inline `as { stop(): Promise<void> }` assertion in index.ts
with a proper ProxyManager import and typed cast.
PERF-1: pre-compute reversed middleware array once in StreamTranslator
constructor rather than allocating a new array on every SSE chunk
(applyMiddleware was called hundreds/thousands of times per streaming
response via runStreamEventMiddleware).
Co-Authored-By: Claude <noreply@anthropic.com>
… to server mode
SEC-2: Add TranslateTarget type union to configuration.ts so the translate field
is typed (not raw string). Validate against supported values at all three entry
points: CLI (agents.ts), MCP Zod schema (z.enum), and config read boundary
(loadAgentConfig). Simplify MCP adapter warning check — truthy test is sufficient
since empty string is falsy.
REG-1: Gate proxy startup on mode === 'server'. CLI modes ('cli', 'run') skip
proxy startup — their spawned processes read their own config. Guard changed from
`!options.processSpawner` to `!options.processSpawner && mode === 'server'`.
ARCH-4: Document the optional untyped proxyManager container registration so
future developers understand callers must handle the missing-key case.
ARCH-SF1: Expand ARCHITECTURE comment on the temporal dependency between proxy
startup and agentRegistry registration — clarifies the ordering constraint and
the per-request middlewareFactory pattern.
Also completes the middlewares → middlewareFactory rename in translation-proxy.ts
and proxy-manager.ts (incomplete refactor that blocked typecheck).
Co-Authored-By: Claude <noreply@anthropic.com>
…lpers, add guards - Extract buildToolResultMessages() and buildAssistantMessage() from buildOpenAIMessages() (COMP-4): reduces function from ~88 lines with 3 continue statements to a clean 20-line dispatch loop - Extract closeActiveTextBlock(), closeActiveToolCall(), and processToolCallDeltas() from OpenAIStreamParser.processChunk (COMP-3): reduces 152-line method with cyclomatic complexity ~18 to a focused 60-line orchestrator with named helpers - Replace non-null assertion on activeToolCalls.get() with explicit guard (TS-2): malformed streams with continuing deltas after re-keying now safely continue rather than throw - Use type predicate filters (TS-3): filter((c): c is Extract<...>) + direct map eliminates redundant type checks and unreachable fallback returns in tool_use and tool_result serialization paths Co-Authored-By: Claude <noreply@anthropic.com>
…xity, security ARCH-1/2/3: Replace shared middlewares array with per-request middlewareFactory in TranslationProxyConfig. handleMessages() now calls middlewareFactory() once per request so LoggingMiddleware, PromptCacheMiddleware, and ToolNameMappingMiddleware each get isolated mutable state — no cross-request data races under concurrency. COMP-1+PERF-2: Extract handleStreamingError, handleJsonFallback, handleSseStream from handleStreamingRequest (was 161 lines, 5 levels). SSE writes are now batched per backend chunk (one res.write per chunk instead of one per line). COMP-2: Extract processNonStreamingResponse from handleNonStreamingRequest success path (was 104 lines, 5 levels). Both streaming and non-streaming JSON paths share the same parse→middleware→serialize→send helper. COMP-5: Extract countApproxChars() pure function from handleCountTokens (was 48 lines, 5 levels). Character counting is now separately testable. SEC-1: Sanitize req.url before including in 404 error message — strip non-printable ASCII and cap at 200 chars to prevent log injection. Co-Authored-By: Claude <noreply@anthropic.com>
…ror paths, and streaming fallback - Add middleware.test.ts: unit tests for runRequestMiddleware (forward order), runResponseMiddleware (reverse order), runStreamEventMiddleware (reverse order, null-drop, short-circuit), and no-op middleware skipping - Fix prompt-cache.test.ts: strengthen weak toBeGreaterThanOrEqual(0) assertion to toBe(15) to verify no-double-counting invariant - Add error path tests in translation-proxy.test.ts: 405 non-POST, 404 unknown endpoint, 400 invalid JSON body, 502 connection refused, 502 invalid JSON response - Add streaming JSON fallback test: backend returning application/json to a stream request is translated as a normal response (handleJsonFallback path) - Add DECISION comment at TestableProxiedClaudeAdapter class definition for project convention compliance Co-Authored-By: Claude <noreply@anthropic.com>
…ation, promote constant
…al index mapping Add openaiToCanonicalIndex and pendingToolCalls maps to OpenAIStreamParser so that subsequent delta chunks arriving with the original OpenAI tcIndex are correctly resolved to the canonical content index after re-keying. Previously, re-keying deleted the activeToolCalls entry at tcIndex, causing lookup failures and silently lost arguments on continuation chunks. Co-Authored-By: Claude <noreply@anthropic.com>
… generic string Add extractBackendErrorMessage() helper that parses the backend response body to extract a human-readable error message (OpenAI format: error.message, plain message field, or raw text). Messages are truncated to 500 chars and fall back to 'Backend returned error' on empty body. Used in both non-streaming and streaming error paths. Adds logger.debug tracing for backend error responses. Co-Authored-By: Claude <noreply@anthropic.com>
- Move substring truncation after JSON.parse in extractBackendErrorMessage to prevent truncation from breaking valid JSON before parsing - Remove dead code branch in pending tool call handler (existing.started can never be true while in pendingToolCalls map) - Simplify type casting in extractBackendErrorMessage
Code Review SummaryConsolidated findings from 8 reviewers (≥80% confidence). Filtering for blocking/high-severity issues: 🔴 Blocking Issues (Must Fix Before Merge)
🟡 High-Severity Issues (Strong Recommendation to Fix)
📋 Summary by Category
Review Process Notes
Claude Code Code Review Agent • Confidence-weighted filtering applied |
…t IR type
ContentDeltaEvent is flat { type, index, text } — no nested delta property.
Fix makeStreamEvent(), makeStreamTagger(), and all assertions that were
accessing the non-existent event.delta.text path.
Co-Authored-By: Claude <noreply@anthropic.com>
|
CRITICAL: Database registration throws instead of returning Line 274: The Fix: Wrap the re-throw in an } catch (error: unknown) {
const msg = error instanceof Error ? error.message : String(error);
if (msg.includes("NODE_MODULE_VERSION")) {
return err(new AutobeatError(...));
}
return err(
new AutobeatError(
ErrorCode.SYSTEM_ERROR,
`Failed to initialize database: ${msg}`,
),
);
} |
|
HIGH: Database registration pattern deviation needs DECISION comment Lines 256-275: Registration changed from lazy |
|
HIGH: URL construction bypasses URL API — src/translation/proxy/translation-proxy.ts:389 String concatenation const base = new URL(this.config.targetBaseUrl);
base.pathname = base.pathname.replace(/\/?$/, "") + "/chat/completions"; |
|
HIGH: Non-streaming response accumulation unbounded — src/translation/proxy/translation-proxy.ts:484-488, 584-589 Success path accumulates chunks with no size cap, unlike error path (MAX_ERR_BYTES) and inbound limit (MAX_BODY_BYTES). A malicious backend could send very large responses without bounds. Fix: Add size cap on success paths matching inbound limit: const chunks: Buffer[] = [];
let totalBytes = 0;
backendRes.on("data", (chunk: Buffer) => {
totalBytes += chunk.length;
if (totalBytes <= MAX_BODY_BYTES) chunks.push(chunk);
});
backendRes.on("end", () => {
if (totalBytes > MAX_BODY_BYTES) {
sendError(res, 502, "api_error", "Backend response too large");
return;
}
// ... process response
}); |
|
HIGH: serializeContentBlock missing exhaustive never check — src/translation/codecs/anthropic-codec.ts:178 The switch statement uses a silent fallback Fix: Add exhaustive check: case "image":
case "document":
case "json":
case "tool_result":
return { type: "text", text: "" };
default: {
const _exhaustive: never = content;
return { type: "text", text: "" };
} |
|
MEDIUM: TLS bypass suggestion in error message — src/utils/url-probe.ts:146 Confidence: 90%. The error suggests Fix: Replace with safer recommendation: return `TLS/SSL error connecting to ${urlStr}: ${error.message}. Verify the servers TLS certificate is valid, or configure a custom CA bundle via NODE_EXTRA_CA_CERTS.`; |
|
MEDIUM: No protocol scheme restriction on probeUrl — src/utils/url-probe.ts:212-221 Confidence: 82%. The function validates URL parsing but allows any scheme (file://, ftp://, etc.). While the URL eventually goes to http.request which would fail, defense-in-depth would reject non-HTTP schemes at the probe entry point. Fix: Add scheme validation: if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") {
return err(new Error(`Unsupported protocol "${parsedUrl.protocol}" — only http: and https: are supported`));
} |
|
MEDIUM: rawError cast bypasses unknown narrowing — src/utils/url-probe.ts:101 Confidence: 82%. Line 101 casts Fix: Narrow properly before accessing ErrnoException fields: const error = rawError instanceof Error
? (rawError as NodeJS.ErrnoException)
: Object.assign(new Error(String(rawError)), { code: undefined }) as NodeJS.ErrnoException; |
|
HIGH: handleConfigureAgent exceeds complexity threshold — src/adapters/mcp-adapter.ts:3326 Confidence: 88%. The method spans 220+ lines with 3 case branches and cyclomatic complexity ~15. The Fix: Extract each switch case into dedicated private methods: private async handleConfigureAgent(args: unknown): Promise<MCPToolResponse> {
const { agent, action, ...fields } = ConfigureAgentSchema.safeParse(args).data;
switch (action) {
case "check": return this.handleConfigureAgentCheck(agent);
case "set": return this.handleConfigureAgentSet(agent, fields);
case "reset": return this.handleConfigureAgentReset(agent);
}
}This keeps each method under 30 lines and improves maintainability. |
|
HIGH: probeUrl adds 5s latency to ConfigureAgent check — src/adapters/mcp-adapter.ts:3357-3364 Confidence: 82%. The Fix: Make probe optional via tool schema parameter, or reduce timeout to 2-3s for check action, or make probe fire-and-forget. The |
|
MEDIUM: Inconsistent response typing in ConfigureAgent — src/adapters/mcp-adapter.ts:3521-3536 Confidence: 82%. The Fix: Either remove |
|
MEDIUM: argumentsAccumulator string concat causes O(n²) behavior — src/translation/codecs/openai-codec.ts:353,381 Confidence: 80%. Tool call arguments accumulate via Fix: Use array accumulator and join at the end: interface ActiveToolCall {
id: string;
name: string;
argumentsChunks: string[]; // was argumentsAccumulator
started: boolean;
}
// On delta: tc.argumentsChunks.push(tcArgs);
// On stop: arguments: tc.argumentsChunks.join("") |
|
MEDIUM: probeUrl silently swallows deep-probe network errors — src/utils/url-probe.ts:245-247 Confidence: 82%. When the deep probe (GET /models) fails with a network error but the base HEAD probe succeeded, the error is silently discarded. This loses diagnostic information about authentication failures. Fix: Include a warning in the returned result: if ("error" in deepResult) {
const baseMessage = messageForStatus(statusCode, headers, parsedUrl, false);
return ok({
reachable: true,
statusCode,
message: `${baseMessage}. Note: API key validation failed (${deepResult.error.message})`,
severity: "warning",
durationMs,
});
} |
|
MEDIUM: messageForError uses 6 if-chains instead of lookup — src/utils/url-probe.ts:121-151 Confidence: 82%. The function has cyclomatic complexity ~8 with a long chain of if/else-if blocks. The TLS error check alone is a 7-operand disjunction (lines 137-144). Each new error code extends the chain linearly. Fix: Extract TLS codes to a Set and consider a dispatch table: const TLS_ERROR_CODES = new Set([
"CERT_HAS_EXPIRED", "CERT_INVALID",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"SELF_SIGNED_CERT_IN_CHAIN", "DEPTH_ZERO_SELF_SIGNED_CERT",
]);
function isTlsError(code: string): boolean {
return TLS_ERROR_CODES.has(code) || code.startsWith("ERR_TLS") || code.startsWith("ERR_SSL");
} |
|
HIGH: ThinkingDeltaEvent type change is breaking — src/translation/ir.ts:218-222 Confidence: 90%. The Note: If If external consumers exist, this breaking change needs a CHANGELOG note and migration guide. |
Review Summary: PR #152Overall Status: Changes Requested (15 high-confidence issues found) Issue Breakdown by Severity
Lower-Confidence Suggestions (60-79%, for consideration)
Consolidated FindingsMultiple reviewers independently flagged the same issues, confirming high confidence:
Next Steps
Reviewed by 9 independent agents (Security, Architecture, Performance, Complexity, Consistency, Regression, Testing, TypeScript, Dependencies) |
…ation - Replace `throw error` on the non-ABI database init failure path with `return err(...)`, matching every other error path in bootstrap() and honouring the Result<Container> contract. - Add DECISION comment on the registerValue block explaining why eager instantiation is used: ABI mismatch detection must surface at startup, not on first database access deep inside request handling. - Add inline comment on the proxy logger.info call noting that proxyConfig.targetApiKey is intentionally omitted to prevent future accidental secret exposure. Co-Authored-By: Claude <noreply@anthropic.com>
… URL Add MAX_BODY_BYTES size cap to the non-streaming success path and handleJsonFallback — matching the existing cap on the error path and the inbound request limit. Without this cap, a malformed or adversarial backend response could exhaust heap memory. Pre-compute the chat completions URL once in the constructor instead of rebuilding it on every request, eliminating redundant string operations and using the URL API's correct edge-case handling. Co-Authored-By: Claude <noreply@anthropic.com>
- inline CheckPayload type annotation to match removal of SetPayload (both are now inline object types — symmetric typing) - switch agents.ts probeUrl to static import, consistent with mcp-adapter - add DESIGN comments explaining intentional check vs set probe asymmetry: check includes full connectivity diagnostics for user inspection; set only warns on non-ok probe since the save already succeeded Co-Authored-By: Claude <noreply@anthropic.com>
- anthropic-codec: replace silent default fallback in serializeContentBlock with explicit cases for image, document, json, and tool_result (all have no Anthropic wire equivalent) plus a never exhaustiveness guard so the compiler catches any future CanonicalContent variants - openai-codec: change ActiveToolCall.argumentsAccumulator from string (O(n²) concatenation) to string[] accumulated via push and joined once at close time, fixing the perf issue for large tool call argument streams Co-Authored-By: Claude <noreply@anthropic.com>
Security:
- Replace NODE_TLS_REJECT_UNAUTHORIZED=0 suggestion with NODE_EXTRA_CA_CERTS
to avoid disabling all certificate verification globally
- Restrict probeUrl to http:/https: schemes only; file:// and ftp:// now
return err() immediately preventing silent misbehaviour
TypeScript:
- Narrow rawError with instanceof Error before casting to ErrnoException,
eliminating the unsafe blind cast at the error handler boundary
Architecture:
- Add deepProbeWarning?: string to UrlProbeResult; populated when the deep
probe (GET /models) fails with a network error after HEAD succeeds, so
callers can surface the previously-silent failure
Complexity:
- Extract TLS_ERROR_CODES named Set constant with helper isTlsError(); removes
the 7-operand TLS check from messageForError
- Combine messageForStatus + severityForStatus into single statusResult()
returning { message, severity }; eliminates duplicated status code dispatch
Tests:
- Add tests for file:// and ftp:// scheme rejection
- Add test verifying deepProbeWarning is set on deep probe network failure
Co-Authored-By: Claude <noreply@anthropic.com>
## Summary Release v1.5.0 — Cross-Platform Agents & Interactive Orchestration - API translation proxy — Anthropic Messages API to OpenAI Chat Completions (#152) - Dashboard layout overhaul — 3-tile responsive, full-width entity browser (#153) - Ollama runtime + translate→proxy rename (#157) - Skills/docs alignment (#158) - Interactive orchestrator mode --interactive/-i (#159) 8 files updated (version bump, release notes, changelog, features, roadmap, CLAUDE.md) 2 new migrations (v24, v25); 3 new MCP tools (PipelineStatus, ListPipelines, CancelPipeline) ### Snyk (best-effort) 2 medium findings — both pre-existing: - HTTP instead of HTTPS in translation-proxy.ts (by design: localhost proxy) - Path traversal in orchestrate.ts (pre-existing code path, only imports changed) ## Test plan - [x] typecheck + lint + build pass - [x] All 14 grouped test suites pass (0 failures) - [x] Release notes index matches files on disk (patch releases excluded by convention) - [ ] CI passes Co-authored-by: Dean Sharon <deanshrn@gmain.com>
Summary
Implement a translation proxy infrastructure that enables API translation from other LLM providers to Claude SDK-compatible JSON. This supports multi-agent orchestration across different provider ecosystems (OpenAI, Anthropic SDK, Codex, Gemini, etc.).
Changes
translatefield in AgentConfigBreaking Changes
None
Testing
Related Issues
Closes #152 (API translation proxy core)