fix(small-model): harden configured headers

This commit is contained in:
Bohdan Triapitsyn
2026-08-28 23:50:04 +03:00
parent 46284301b8
commit 2a5a25d5a2
2 changed files with 94 additions and 11 deletions
+25 -9
View File
@@ -19,6 +19,18 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 4_000;
const USER_AGENT = 'opencode/1.0 openchamber';
const mergeHeadersCaseInsensitive = (base, overrides) => {
const merged = { ...base };
for (const [name, value] of Object.entries(overrides || {})) {
const existingName = Object.keys(merged).find((key) => key.toLowerCase() === name.toLowerCase());
if (existingName) {
delete merged[existingName];
}
merged[name] = value;
}
return merged;
};
const CODEX_TOKEN_URL = 'https://auth.openai.com/oauth/token';
const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
@@ -157,11 +169,10 @@ const callOpenaiCompatible = async ({ baseURL, headers, modelID, prompt, system,
});
const response = await fetch(`${trimmedBase}/chat/completions`, {
method: 'POST',
headers: {
headers: mergeHeadersCaseInsensitive({
'Content-Type': 'application/json',
Accept: 'application/json',
...headers,
},
}, headers),
body: JSON.stringify({
model: modelID,
messages: [
@@ -510,7 +521,7 @@ const callCodexResponses = async ({ accessToken, accountId, modelID, prompt, sys
// Custom provider configuration support
// ---------------------------------------------------------------------------
const resolveConfigApiKey = (value, workingDirectory, providerID) => {
const resolveConfigValue = (value, workingDirectory, providerID, headerName = null) => {
const envMatch = value.match(/^\{env:([^}]+)\}$/i);
if (envMatch) {
return process.env[envMatch[1].trim()]?.trim() || null;
@@ -531,7 +542,12 @@ const resolveConfigApiKey = (value, workingDirectory, providerID) => {
{ config: layers.customConfig, filePath: layers.paths.customPath },
{ config: layers.projectConfig, filePath: layers.paths.projectPath },
{ config: layers.userConfig, filePath: layers.paths.userPath },
].find(({ config }) => config?.provider?.[providerID]?.options?.apiKey === value);
].find(({ config }) => {
const options = config?.provider?.[providerID]?.options;
return headerName
? options?.headers?.[headerName] === value
: options?.apiKey === value;
});
resolvedPath = path.resolve(source?.filePath ? path.dirname(source.filePath) : workingDirectory || process.cwd(), configuredPath);
}
@@ -540,7 +556,7 @@ const resolveConfigApiKey = (value, workingDirectory, providerID) => {
if (!key) throw new Error('empty file');
return key;
} catch {
throw new Error(`Failed to resolve configured apiKey file for provider "${providerID}"`);
throw new Error(`Failed to resolve configured ${headerName ? `header "${headerName}"` : 'apiKey'} file for provider "${providerID}"`);
}
};
@@ -561,7 +577,7 @@ const readConfiguredHeaders = (providerCfg, workingDirectory, providerID) => {
// Config headers are strings; a malformed entry is skipped rather than
// stringified into a header the gateway would reject.
if (String(value) !== value) continue;
const resolved = resolveConfigApiKey(value.trim(), workingDirectory, providerID);
const resolved = resolveConfigValue(value.trim(), workingDirectory, providerID, name);
if (resolved) headers[name] = resolved;
}
return Object.keys(headers).length ? headers : null;
@@ -574,7 +590,7 @@ const readProviderConfig = (workingDirectory, providerID) => {
if (!providerCfg || typeof providerCfg !== 'object') return null;
const baseURL = typeof providerCfg?.options?.baseURL === 'string' ? providerCfg.options.baseURL.trim() : null;
const rawApiKey = typeof providerCfg?.options?.apiKey === 'string' ? providerCfg.options.apiKey.trim() : null;
const apiKey = rawApiKey ? resolveConfigApiKey(rawApiKey, workingDirectory, providerID) : null;
const apiKey = rawApiKey ? resolveConfigValue(rawApiKey, workingDirectory, providerID) : null;
return {
baseURL,
headers: readConfiguredHeaders(providerCfg, workingDirectory, providerID),
@@ -779,7 +795,7 @@ export async function callSmallModel({ auth, catalog, workingDirectory, provider
baseURL,
// Configured headers last: a gateway that authenticates on its own header
// must be able to override the bearer default rather than sit beside it.
headers: { Authorization: `Bearer ${apiKey}`, ...(providerConfig?.headers || {}) },
headers: mergeHeadersCaseInsensitive({ Authorization: `Bearer ${apiKey}` }, providerConfig?.headers),
modelID,
prompt,
system,