@@ -29,6 +29,7 @@ import {
2929import type { CodexProfile } from "../auth/codex/store.js" ;
3030import type { XaiProfile } from "../auth/xai/store.js" ;
3131import { listCodexProfiles , listXaiProfiles } from "./oauth-stores.js" ;
32+ import { registerSourceCredential } from "./source-credentials.js" ;
3233import {
3334 codexProfilesToCatalogEntries ,
3435 codexProvidersAsSettings ,
@@ -105,12 +106,22 @@ import { resolveProfile } from "./profiles.js";
105106// revert the ceiling.
106107export const SOURCE_MAX_TOKENS = 16384 ;
107108
108- // Placeholder sent in the Authorization header for keyless local providers
109- // (e.g. Ollama). The runtime's InferenceSource type requires a non-empty
110- // apiKey string; the value is injected as `Bearer <key>` by the harness but
111- // keyless servers ignore it entirely.
109+ // Placeholder resolved from the credential cell for keyless local providers
110+ // (e.g. Ollama). Sources that need no secret register this sentinel; the
111+ // harness still sends it as `Bearer <key>` but keyless servers ignore it.
112112export const KEYLESS_API_KEY = "keyless" ;
113113
114+ // Registers the secret behind a source id in the credential cell (see
115+ // ./source-credentials.ts), falling back to the keyless sentinel when no key
116+ // was configured. Every buildXSource below calls this so the vendored
117+ // credentialId auth model resolves the secret at send time.
118+ function registerSourceSecret ( id : string , apiKey : string | undefined ) : void {
119+ registerSourceCredential (
120+ id ,
121+ apiKey !== undefined && apiKey . length > 0 ? apiKey : KEYLESS_API_KEY ,
122+ ) ;
123+ }
124+
114125function applyPersistedOAuthDefaults (
115126 settings : Settings | null ,
116127 projected : Record < string , ProviderSettings > ,
@@ -253,16 +264,14 @@ export function buildOpenAISource(fields: {
253264 fields . reasoningEffort !== undefined
254265 ? { providerOptions : { reasoning_effort : fields . reasoningEffort } }
255266 : { } ;
267+ registerSourceSecret ( fields . id , fields . apiKey ) ;
256268 return {
257269 id : fields . id ,
258270 provider : "openai-compatible" ,
259271 baseURL : isOllamaProviderId ( fields . id )
260272 ? ollamaOpenAIBaseURL ( fields . baseURL )
261273 : normalizeOpenAICompatibleBaseURL ( fields . baseURL ) ,
262- apiKey :
263- fields . apiKey !== undefined && fields . apiKey . length > 0
264- ? fields . apiKey
265- : KEYLESS_API_KEY ,
274+ credentialId : fields . id ,
266275 model : fields . model ,
267276 defaults : { maxTokens : SOURCE_MAX_TOKENS , ...overrides } ,
268277 ...( fields . quirks !== undefined ? { quirks : fields . quirks } : { } ) ,
@@ -318,7 +327,8 @@ export type ProviderCatalogEntry = Omit<
318327// "codex-responses" adapter (the Codex backend speaks the Responses API, not
319328// Chat Completions) and carries the account id + a session id through
320329// providerOptions, where the adapter lifts them into request headers. The
321- // access token is the apiKey; the harness injects it as the bearer credential.
330+ // access token is registered in the credential cell under the source id; the
331+ // harness resolves it as the bearer credential at send time.
322332export function buildCodexSource ( fields : {
323333 id : string ;
324334 apiKey : string ;
@@ -334,19 +344,21 @@ export function buildCodexSource(fields: {
334344 providerOptions [ CODEX_ACCOUNT_ID_OPTION ] = fields . accountId ;
335345 if ( fields . reasoningEffort !== undefined )
336346 providerOptions [ "reasoning_effort" ] = fields . reasoningEffort ;
347+ registerSourceSecret ( fields . id , fields . apiKey ) ;
337348 return {
338349 id : fields . id ,
339350 provider : CODEX_RESPONSES_PROVIDER ,
340351 baseURL : CODEX_BASE_URL ,
341- apiKey : fields . apiKey ,
352+ credentialId : fields . id ,
342353 model : fields . model ,
343354 defaults : { maxTokens : SOURCE_MAX_TOKENS , providerOptions } ,
344355 } ;
345356}
346357
347358// Build the InferenceSource for an xAI/Grok OAuth profile. Routes to the
348359// "grok-responses" adapter (the grok-cli proxy speaks the Responses API, not
349- // Chat Completions). The access token is the apiKey; the caller's user id is
360+ // Chat Completions). The access token is registered in the credential cell
361+ // under the source id; the caller's user id is
350362// decoded from it and lifted into the x-grok-user-id header by the adapter.
351363// The session id becomes the request's prompt_cache_key so every call in the
352364// thread routes to the same cache shard (store:false has no other signal).
@@ -364,11 +376,12 @@ export function buildXaiSource(fields: {
364376 if ( userId !== undefined ) providerOptions [ GROK_USER_ID_OPTION ] = userId ;
365377 if ( fields . reasoningEffort !== undefined )
366378 providerOptions [ "reasoning_effort" ] = fields . reasoningEffort ;
379+ registerSourceSecret ( fields . id , fields . apiKey ) ;
367380 return {
368381 id : fields . id ,
369382 provider : GROK_RESPONSES_PROVIDER ,
370383 baseURL : XAI_BASE_URL ,
371- apiKey : fields . apiKey ,
384+ credentialId : fields . id ,
372385 model : fields . model ,
373386 defaults : { maxTokens : SOURCE_MAX_TOKENS , providerOptions } ,
374387 } ;
@@ -388,14 +401,12 @@ export function buildBifrostSource(fields: {
388401 fields . reasoningEffort !== undefined
389402 ? { providerOptions : { reasoning_effort : fields . reasoningEffort } }
390403 : { } ;
404+ registerSourceSecret ( fields . id , fields . apiKey ) ;
391405 return {
392406 id : fields . id ,
393407 provider : BIFROST_PROVIDER ,
394408 baseURL : normalizeOpenAICompatibleBaseURL ( fields . baseURL ) ,
395- apiKey :
396- fields . apiKey !== undefined && fields . apiKey . length > 0
397- ? fields . apiKey
398- : KEYLESS_API_KEY ,
409+ credentialId : fields . id ,
399410 model : fields . model ,
400411 defaults : { maxTokens : SOURCE_MAX_TOKENS , ...overrides } ,
401412 } ;
@@ -408,14 +419,12 @@ export function buildAnthropicSource(fields: {
408419 apiKey ?: string ;
409420 model : string ;
410421} ) : InferenceSource {
422+ registerSourceSecret ( fields . id , fields . apiKey ) ;
411423 return {
412424 id : fields . id ,
413425 provider : "anthropic" ,
414426 baseURL : fields . baseURL . replace ( / \/ + $ / , "" ) ,
415- apiKey :
416- fields . apiKey !== undefined && fields . apiKey . length > 0
417- ? fields . apiKey
418- : KEYLESS_API_KEY ,
427+ credentialId : fields . id ,
419428 model : fields . model ,
420429 defaults : { maxTokens : SOURCE_MAX_TOKENS } ,
421430 } ;
@@ -431,16 +440,13 @@ export function buildGoSource(fields: {
431440 reasoningEffort ?: ReasoningEffort ;
432441} ) : InferenceSource {
433442 const endpoint = resolveGoEndpoint ( fields . model ) ;
434- const apiKey =
435- fields . apiKey !== undefined && fields . apiKey . length > 0
436- ? fields . apiKey
437- : KEYLESS_API_KEY ;
443+ registerSourceSecret ( fields . id , fields . apiKey ) ;
438444 if ( endpoint . adapter === "anthropic" ) {
439445 return {
440446 id : fields . id ,
441447 provider : OPENCODE_GO_MESSAGES_PROVIDER ,
442448 baseURL : endpoint . baseURL ,
443- apiKey ,
449+ credentialId : fields . id ,
444450 model : fields . model ,
445451 defaults : {
446452 maxTokens : SOURCE_MAX_TOKENS ,
@@ -455,7 +461,7 @@ export function buildGoSource(fields: {
455461 id : fields . id ,
456462 provider : OPENAI_RESPONSES_PROVIDER ,
457463 baseURL : endpoint . baseURL ,
458- apiKey ,
464+ credentialId : fields . id ,
459465 model : fields . model ,
460466 defaults : {
461467 maxTokens : SOURCE_MAX_TOKENS ,
@@ -471,7 +477,7 @@ export function buildGoSource(fields: {
471477 id : fields . id ,
472478 baseURL :
473479 endpoint . baseURL . length > 0 ? endpoint . baseURL : OPENCODE_GO_BASE_URL ,
474- apiKey,
480+ ... ( fields . apiKey !== undefined ? { apiKey : fields . apiKey } : { } ) ,
475481 model : fields . model ,
476482 ...( fields . reasoningEffort !== undefined
477483 ? { reasoningEffort : fields . reasoningEffort }
@@ -500,16 +506,13 @@ export function buildZenSource(fields: {
500506 reasoningEffort ?: ReasoningEffort ;
501507} ) : InferenceSource {
502508 const endpoint = resolveZenEndpoint ( fields . model ) ;
503- const apiKey =
504- fields . apiKey !== undefined && fields . apiKey . length > 0
505- ? fields . apiKey
506- : KEYLESS_API_KEY ;
509+ registerSourceSecret ( fields . id , fields . apiKey ) ;
507510 if ( endpoint . adapter === "anthropic" ) {
508511 return {
509512 id : fields . id ,
510513 provider : ZEN_MESSAGES_PROVIDER ,
511514 baseURL : endpoint . baseURL ,
512- apiKey ,
515+ credentialId : fields . id ,
513516 model : fields . model ,
514517 defaults : {
515518 maxTokens : SOURCE_MAX_TOKENS ,
@@ -524,7 +527,7 @@ export function buildZenSource(fields: {
524527 id : fields . id ,
525528 provider : OPENAI_RESPONSES_PROVIDER ,
526529 baseURL : endpoint . baseURL ,
527- apiKey ,
530+ credentialId : fields . id ,
528531 model : fields . model ,
529532 defaults : {
530533 maxTokens : SOURCE_MAX_TOKENS ,
@@ -540,7 +543,7 @@ export function buildZenSource(fields: {
540543 id : fields . id ,
541544 baseURL :
542545 endpoint . baseURL . length > 0 ? endpoint . baseURL : ZEN_DEFAULT_BASE_URL ,
543- apiKey,
546+ ... ( fields . apiKey !== undefined ? { apiKey : fields . apiKey } : { } ) ,
544547 model : fields . model ,
545548 ...( fields . reasoningEffort !== undefined
546549 ? { reasoningEffort : fields . reasoningEffort }
0 commit comments