fix(search): allowed command-backed codex keys

- Exposed command-backed provider-key detection from ModelRegistry.
- Allowed configured Codex command keys to outrank stored OAuth while preserving the custom-endpoint OAuth guard.
- Added resolver-precedence regression coverage.

Fixes #6001
This commit is contained in:
roboomp
2026-07-18 15:34:03 +00:00
parent feac38298f
commit 2d27bfdd66
4 changed files with 46 additions and 4 deletions
@@ -1986,6 +1986,17 @@ export class ModelRegistry {
);
}
/**
* Whether the provider's configured API key is resolved from a command.
*
* Callers use this to distinguish the registry's command-first resolver
* path from lower-priority credentials in {@link authStorage}.
*/
hasCommandBackedApiKey(provider: string): boolean {
const keyConfig = this.#customProviderApiKeys.get(provider);
return isCommandConfigValue(keyConfig);
}
getDiscoverableProviders(): string[] {
const disabledProviders = getDisabledProviderIdsFromSettings();
return this.#discoverableProviders
@@ -625,12 +625,13 @@ export async function searchCodex(params: SearchParams): Promise<SearchResponse>
let result: CodexSearchResult;
if (transport.customEndpoint) {
// The resolver draws its bearer from the registry's own storage when a
// registry is supplied, so validate the credential origin against that
// same storage — not a caller-supplied `authStorage` that may differ.
// ModelRegistry resolves command-backed provider keys before consulting
// its AuthStorage, so a lower-priority OAuth origin is irrelevant when
// that command source is configured.
const credentialSource = params.modelRegistry?.authStorage ?? params.authStorage;
const credentialOrigin = credentialSource.getCredentialOrigin("openai-codex");
if (credentialOrigin?.kind === "oauth" || credentialOrigin?.kind === "env") {
const hasCommandBackedKey = params.modelRegistry?.hasCommandBackedApiKey("openai-codex") === true;
if (!hasCommandBackedKey && (credentialOrigin?.kind === "oauth" || credentialOrigin?.kind === "env")) {
throw new SearchProviderError(
"codex",
`Refusing to send official Codex OAuth credentials to custom endpoint ${transport.baseUrl}. Configure an API key for provider "openai-codex".`,
@@ -50,6 +50,8 @@ describe("ModelRegistry command-resolved models.yml values", () => {
);
const registry = new ModelRegistry(authStorage, modelsPath);
expect(registry.hasCommandBackedApiKey("anthropic")).toBe(true);
expect(registry.hasCommandBackedApiKey("openai")).toBe(false);
const models = registry.getAll().filter(model => model.provider === "anthropic");
expect(models.length).toBeGreaterThan(1);
@@ -219,6 +219,9 @@ describe("searchCodex model selection", () => {
getProviderHeaders() {
return { "X-Proxy-Tenant": "tenant-1" };
},
hasCommandBackedApiKey() {
return false;
},
resolver() {
return async () => "test-proxy-key";
},
@@ -324,6 +327,31 @@ describe("searchCodex model selection", () => {
expect(fetchMock).not.toHaveBeenCalled();
});
it("prefers a command-backed proxy key over stored OAuth on a custom endpoint", async () => {
process.env.PI_CODEX_WEB_SEARCH_MODEL = "gpt-5.4";
const commandBackedRegistry = {
...proxyModelRegistry,
authStorage: oauthOnlyAuthStorage,
hasCommandBackedApiKey(provider: string) {
return provider === "openai-codex";
},
resolver() {
return async () => "command-proxy-key";
},
} as unknown as ModelRegistry;
const result = await searchCodex({
...makeSearchParams("command proxy key", mockCodexFetch("gpt-5.4")),
authStorage: oauthOnlyAuthStorage,
modelRegistry: commandBackedRegistry,
});
const headers = new Headers(capturedRequest?.headers);
expect(headers.get("authorization")).toBe("Bearer command-proxy-key");
expect(headers.has("chatgpt-account-id")).toBe(false);
expect(result.answer).toBe("Codex answer");
});
it("falls back to the default model when PI_CODEX_WEB_SEARCH_MODEL is blank", async () => {
process.env.PI_CODEX_WEB_SEARCH_MODEL = " ";
const result = await searchCodex(makeSearchParams("blank codex model", mockCodexFetch("gpt-5.6-luna")));