Skip to content

Commit 6ae99bc

Browse files
authored
fix: address review comments from PR #1979
- types.ts: correct credential priority comment to reference user-configurable vars COPILOT_GITHUB_TOKEN/COPILOT_API_KEY instead of internal COPILOT_AUTH_TOKEN - smoke-opencode.md: add note that lock file compilation is pending until opencode engine support lands in gh-aw - server.js: add rate limiting to OpenCode (port 10004) HTTP handler using content-length-aware checkRateLimit() call - server.js: extract resolveOpenCodeRoute() helper for testability and refactor handler to use it - server.test.js: add 8 unit tests covering all OpenCode routing priority scenarios and header injection Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/a0621fda-d1a3-449b-a3da-a9d0331c4c76
1 parent 7c27652 commit 6ae99bc

File tree

4 files changed

+163
-44
lines changed

4 files changed

+163
-44
lines changed

.github/workflows/smoke-opencode.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ post-steps:
6666
echo "Safe output validation passed"
6767
---
6868

69+
> **Note:** This workflow has not yet been compiled to a `.lock.yml` file and is **not active** in GitHub Actions.
70+
> Compilation is pending `opencode` engine support in gh-aw. Once the engine is supported, run
71+
> `gh-aw compile .github/workflows/smoke-opencode.md` followed by
72+
> `npx tsx scripts/ci/postprocess-smoke-workflows.ts` to generate the lock file.
73+
6974
# Smoke Test: OpenCode Engine Validation
7075

7176
**IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.**

containers/api-proxy/server.js

Lines changed: 61 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,33 @@ if (!proxyAgent) {
288288
logRequest('warn', 'startup', { message: 'No HTTPS_PROXY configured, requests will go direct' });
289289
}
290290

291+
/**
292+
* Resolves the OpenCode routing configuration based on available credentials.
293+
* Priority: OPENAI_API_KEY > ANTHROPIC_API_KEY > copilotToken (COPILOT_GITHUB_TOKEN / COPILOT_API_KEY)
294+
*
295+
* @param {string|undefined} openaiKey
296+
* @param {string|undefined} anthropicKey
297+
* @param {string|undefined} copilotToken
298+
* @param {string} openaiTarget
299+
* @param {string} anthropicTarget
300+
* @param {string} copilotTarget
301+
* @param {string} [openaiBasePath]
302+
* @param {string} [anthropicBasePath]
303+
* @returns {{ target: string, headers: Record<string,string>, basePath: string|undefined } | null}
304+
*/
305+
function resolveOpenCodeRoute(openaiKey, anthropicKey, copilotToken, openaiTarget, anthropicTarget, copilotTarget, openaiBasePath, anthropicBasePath) {
306+
if (openaiKey) {
307+
return { target: openaiTarget, headers: { 'Authorization': `Bearer ${openaiKey}` }, basePath: openaiBasePath };
308+
}
309+
if (anthropicKey) {
310+
return { target: anthropicTarget, headers: { 'x-api-key': anthropicKey }, basePath: anthropicBasePath };
311+
}
312+
if (copilotToken) {
313+
return { target: copilotTarget, headers: { 'Authorization': `Bearer ${copilotToken}` }, basePath: undefined };
314+
}
315+
return null;
316+
}
317+
291318
/**
292319
* Check rate limit and send 429 if exceeded.
293320
* Returns true if request was rate-limited (caller should return early).
@@ -1052,52 +1079,44 @@ if (require.main === module) {
10521079
url: logUrl,
10531080
});
10541081

1055-
if (OPENAI_API_KEY) {
1056-
logRequest('info', 'opencode_proxy_header_injection', {
1057-
message: '[OpenCode Proxy] Routing to OpenAI/Copilot via OPENAI_API_KEY',
1058-
target: OPENAI_API_TARGET,
1059-
});
1060-
proxyRequest(req, res, OPENAI_API_TARGET, {
1061-
'Authorization': `Bearer ${OPENAI_API_KEY}`,
1062-
}, 'opencode', OPENAI_API_BASE_PATH);
1063-
} else if (ANTHROPIC_API_KEY) {
1064-
logRequest('info', 'opencode_proxy_header_injection', {
1065-
message: '[OpenCode Proxy] Routing to Anthropic via ANTHROPIC_API_KEY',
1066-
target: ANTHROPIC_API_TARGET,
1067-
});
1068-
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
1069-
if (!req.headers['anthropic-version']) {
1070-
anthropicHeaders['anthropic-version'] = '2023-06-01';
1071-
}
1072-
proxyRequest(req, res, ANTHROPIC_API_TARGET, anthropicHeaders, 'opencode', ANTHROPIC_API_BASE_PATH);
1073-
} else {
1074-
// COPILOT_AUTH_TOKEN only — route to Copilot API target
1075-
logRequest('info', 'opencode_proxy_header_injection', {
1076-
message: '[OpenCode Proxy] Routing to Copilot via COPILOT_AUTH_TOKEN',
1077-
target: COPILOT_API_TARGET,
1078-
});
1079-
proxyRequest(req, res, COPILOT_API_TARGET, {
1080-
'Authorization': `Bearer ${COPILOT_AUTH_TOKEN}`,
1081-
}, 'opencode');
1082+
const parsedContentLength = Number(req.headers['content-length']);
1083+
const contentLength = Number.isFinite(parsedContentLength) && parsedContentLength > 0 ? parsedContentLength : 0;
1084+
if (checkRateLimit(req, res, 'opencode', contentLength)) {
1085+
return;
10821086
}
1087+
1088+
const route = resolveOpenCodeRoute(
1089+
OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_AUTH_TOKEN,
1090+
OPENAI_API_TARGET, ANTHROPIC_API_TARGET, COPILOT_API_TARGET,
1091+
OPENAI_API_BASE_PATH, ANTHROPIC_API_BASE_PATH
1092+
);
1093+
if (!route) return;
1094+
1095+
logRequest('info', 'opencode_proxy_header_injection', {
1096+
message: `[OpenCode Proxy] Routing to ${route.target}`,
1097+
target: route.target,
1098+
});
1099+
1100+
const headers = Object.assign({}, route.headers);
1101+
if (ANTHROPIC_API_KEY && !OPENAI_API_KEY && !req.headers['anthropic-version']) {
1102+
headers['anthropic-version'] = '2023-06-01';
1103+
}
1104+
proxyRequest(req, res, route.target, headers, 'opencode', route.basePath);
10831105
});
10841106

10851107
opencodeServer.on('upgrade', (req, socket, head) => {
1086-
if (OPENAI_API_KEY) {
1087-
proxyWebSocket(req, socket, head, OPENAI_API_TARGET, {
1088-
'Authorization': `Bearer ${OPENAI_API_KEY}`,
1089-
}, 'opencode', OPENAI_API_BASE_PATH);
1090-
} else if (ANTHROPIC_API_KEY) {
1091-
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
1092-
if (!req.headers['anthropic-version']) {
1093-
anthropicHeaders['anthropic-version'] = '2023-06-01';
1094-
}
1095-
proxyWebSocket(req, socket, head, ANTHROPIC_API_TARGET, anthropicHeaders, 'opencode', ANTHROPIC_API_BASE_PATH);
1096-
} else {
1097-
proxyWebSocket(req, socket, head, COPILOT_API_TARGET, {
1098-
'Authorization': `Bearer ${COPILOT_AUTH_TOKEN}`,
1099-
}, 'opencode');
1108+
const route = resolveOpenCodeRoute(
1109+
OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_AUTH_TOKEN,
1110+
OPENAI_API_TARGET, ANTHROPIC_API_TARGET, COPILOT_API_TARGET,
1111+
OPENAI_API_BASE_PATH, ANTHROPIC_API_BASE_PATH
1112+
);
1113+
if (!route) return;
1114+
1115+
const headers = Object.assign({}, route.headers);
1116+
if (ANTHROPIC_API_KEY && !OPENAI_API_KEY && !req.headers['anthropic-version']) {
1117+
headers['anthropic-version'] = '2023-06-01';
11001118
}
1119+
proxyWebSocket(req, socket, head, route.target, headers, 'opencode', route.basePath);
11011120
});
11021121

11031122
opencodeServer.listen(10004, '0.0.0.0', () => {
@@ -1125,4 +1144,4 @@ if (require.main === module) {
11251144
}
11261145

11271146
// Export for testing
1128-
module.exports = { normalizeApiTarget, deriveCopilotApiTarget, deriveGitHubApiTarget, deriveGitHubApiBasePath, normalizeBasePath, buildUpstreamPath, proxyWebSocket, resolveCopilotAuthToken };
1147+
module.exports = { normalizeApiTarget, deriveCopilotApiTarget, deriveGitHubApiTarget, deriveGitHubApiBasePath, normalizeBasePath, buildUpstreamPath, proxyWebSocket, resolveCopilotAuthToken, resolveOpenCodeRoute };

containers/api-proxy/server.test.js

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
const http = require('http');
66
const tls = require('tls');
77
const { EventEmitter } = require('events');
8-
const { normalizeApiTarget, deriveCopilotApiTarget, deriveGitHubApiTarget, deriveGitHubApiBasePath, normalizeBasePath, buildUpstreamPath, proxyWebSocket, resolveCopilotAuthToken } = require('./server');
8+
const { normalizeApiTarget, deriveCopilotApiTarget, deriveGitHubApiTarget, deriveGitHubApiBasePath, normalizeBasePath, buildUpstreamPath, proxyWebSocket, resolveCopilotAuthToken, resolveOpenCodeRoute } = require('./server');
99

1010
describe('normalizeApiTarget', () => {
1111
it('should strip https:// prefix', () => {
@@ -781,3 +781,98 @@ describe('resolveCopilotAuthToken', () => {
781781
});
782782
});
783783

784+
describe('resolveOpenCodeRoute', () => {
785+
const OPENAI_TARGET = 'api.openai.com';
786+
const ANTHROPIC_TARGET = 'api.anthropic.com';
787+
const COPILOT_TARGET = 'api.githubcopilot.com';
788+
const OPENAI_BASE = '/v1';
789+
const ANTHROPIC_BASE = '';
790+
791+
it('should route to OpenAI when OPENAI_API_KEY is set (highest priority)', () => {
792+
const route = resolveOpenCodeRoute(
793+
'sk-openai-key', 'sk-anthropic-key', 'gho_copilot-token',
794+
OPENAI_TARGET, ANTHROPIC_TARGET, COPILOT_TARGET,
795+
OPENAI_BASE, ANTHROPIC_BASE
796+
);
797+
expect(route).not.toBeNull();
798+
expect(route.target).toBe(OPENAI_TARGET);
799+
expect(route.headers['Authorization']).toBe('Bearer sk-openai-key');
800+
expect(route.basePath).toBe(OPENAI_BASE);
801+
});
802+
803+
it('should route to Anthropic when only ANTHROPIC_API_KEY is set', () => {
804+
const route = resolveOpenCodeRoute(
805+
undefined, 'sk-anthropic-key', undefined,
806+
OPENAI_TARGET, ANTHROPIC_TARGET, COPILOT_TARGET,
807+
OPENAI_BASE, ANTHROPIC_BASE
808+
);
809+
expect(route).not.toBeNull();
810+
expect(route.target).toBe(ANTHROPIC_TARGET);
811+
expect(route.headers['x-api-key']).toBe('sk-anthropic-key');
812+
expect(route.basePath).toBe(ANTHROPIC_BASE);
813+
});
814+
815+
it('should prefer OpenAI over Anthropic when both are set', () => {
816+
const route = resolveOpenCodeRoute(
817+
'sk-openai-key', 'sk-anthropic-key', undefined,
818+
OPENAI_TARGET, ANTHROPIC_TARGET, COPILOT_TARGET,
819+
OPENAI_BASE, ANTHROPIC_BASE
820+
);
821+
expect(route).not.toBeNull();
822+
expect(route.target).toBe(OPENAI_TARGET);
823+
expect(route.headers['Authorization']).toBe('Bearer sk-openai-key');
824+
});
825+
826+
it('should route to Copilot when only copilotToken is set', () => {
827+
const route = resolveOpenCodeRoute(
828+
undefined, undefined, 'gho_copilot-token',
829+
OPENAI_TARGET, ANTHROPIC_TARGET, COPILOT_TARGET,
830+
OPENAI_BASE, ANTHROPIC_BASE
831+
);
832+
expect(route).not.toBeNull();
833+
expect(route.target).toBe(COPILOT_TARGET);
834+
expect(route.headers['Authorization']).toBe('Bearer gho_copilot-token');
835+
expect(route.basePath).toBeUndefined();
836+
});
837+
838+
it('should prefer Anthropic over Copilot when both are set', () => {
839+
const route = resolveOpenCodeRoute(
840+
undefined, 'sk-anthropic-key', 'gho_copilot-token',
841+
OPENAI_TARGET, ANTHROPIC_TARGET, COPILOT_TARGET,
842+
OPENAI_BASE, ANTHROPIC_BASE
843+
);
844+
expect(route).not.toBeNull();
845+
expect(route.target).toBe(ANTHROPIC_TARGET);
846+
expect(route.headers['x-api-key']).toBe('sk-anthropic-key');
847+
});
848+
849+
it('should return null when no credentials are available', () => {
850+
const route = resolveOpenCodeRoute(
851+
undefined, undefined, undefined,
852+
OPENAI_TARGET, ANTHROPIC_TARGET, COPILOT_TARGET,
853+
OPENAI_BASE, ANTHROPIC_BASE
854+
);
855+
expect(route).toBeNull();
856+
});
857+
858+
it('should not set Authorization header for Anthropic route', () => {
859+
const route = resolveOpenCodeRoute(
860+
undefined, 'sk-anthropic-key', undefined,
861+
OPENAI_TARGET, ANTHROPIC_TARGET, COPILOT_TARGET,
862+
OPENAI_BASE, ANTHROPIC_BASE
863+
);
864+
expect(route).not.toBeNull();
865+
expect(route.headers['Authorization']).toBeUndefined();
866+
});
867+
868+
it('should not set x-api-key header for OpenAI route', () => {
869+
const route = resolveOpenCodeRoute(
870+
'sk-openai-key', undefined, undefined,
871+
OPENAI_TARGET, ANTHROPIC_TARGET, COPILOT_TARGET,
872+
OPENAI_BASE, ANTHROPIC_BASE
873+
);
874+
expect(route).not.toBeNull();
875+
expect(route.headers['x-api-key']).toBeUndefined();
876+
});
877+
});
878+

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const API_PROXY_PORTS = {
4141

4242
/**
4343
* OpenCode API proxy port (defaults to Copilot/OpenAI routing; falls back to Anthropic)
44-
* OpenCode is BYOK — credential priority: OPENAI_API_KEY > ANTHROPIC_API_KEY > COPILOT_AUTH_TOKEN
44+
* OpenCode is BYOK — credential priority: OPENAI_API_KEY > ANTHROPIC_API_KEY > COPILOT_GITHUB_TOKEN/COPILOT_API_KEY
4545
* @see containers/api-proxy/server.js
4646
*/
4747
OPENCODE: 10004,

0 commit comments

Comments
 (0)