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:
@@ -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")));
|
||||
|
||||
Reference in New Issue
Block a user