Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 62 additions & 28 deletions docs/VENDORING.md

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
SOURCE_MAX_TOKENS,
} from "./config/index.js";
import { DIRECTOR_IDS } from "./agent/directors/types.js";
import {
clearSourceCredentials,
peekSourceCredentialSecret,
} from "./config/source-credentials.js";
import type { Config, UnconfiguredConfig } from "./config/index.js";
import {
mergeProviderIntoSettings,
Expand Down Expand Up @@ -69,6 +73,7 @@ afterEach(() => {
resetGoModelDiscoveryForTests();
resetZenModelDiscoveryForTests();
setProviderContextWindowOverrides(undefined);
clearSourceCredentials();
});

function assertConfigured(
Expand Down Expand Up @@ -1505,13 +1510,16 @@ describe("buildOpenAISource", () => {
expect(source.baseURL).toBe("http://localhost:11434/v1");
});

test("substitutes a placeholder apiKey when none is provided (keyless)", () => {
test("registers the keyless placeholder in the credential cell when none is provided", () => {
const source = buildOpenAISource({
id: "local",
baseURL: "http://localhost:8080/v1",
model: "local-model",
});
expect(source.apiKey).toBe(KEYLESS_API_KEY);
expect(source.credentialId).toBe("local");
expect(peekSourceCredentialSecret(source.credentialId)).toBe(
KEYLESS_API_KEY,
);
});
});

Expand Down
71 changes: 37 additions & 34 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import type { CodexProfile } from "../auth/codex/store.js";
import type { XaiProfile } from "../auth/xai/store.js";
import { listCodexProfiles, listXaiProfiles } from "./oauth-stores.js";
import { registerSourceCredential } from "./source-credentials.js";
import {
codexProfilesToCatalogEntries,
codexProvidersAsSettings,
Expand Down Expand Up @@ -105,12 +106,22 @@ import { resolveProfile } from "./profiles.js";
// revert the ceiling.
export const SOURCE_MAX_TOKENS = 16384;

// Placeholder sent in the Authorization header for keyless local providers
// (e.g. Ollama). The runtime's InferenceSource type requires a non-empty
// apiKey string; the value is injected as `Bearer <key>` by the harness but
// keyless servers ignore it entirely.
// Placeholder resolved from the credential cell for keyless local providers
// (e.g. Ollama). Sources that need no secret register this sentinel; the
// harness still sends it as `Bearer <key>` but keyless servers ignore it.
export const KEYLESS_API_KEY = "keyless";

// Registers the secret behind a source id in the credential cell (see
// ./source-credentials.ts), falling back to the keyless sentinel when no key
// was configured. Every buildXSource below calls this so the vendored
// credentialId auth model resolves the secret at send time.
function registerSourceSecret(id: string, apiKey: string | undefined): void {
registerSourceCredential(
id,
apiKey !== undefined && apiKey.length > 0 ? apiKey : KEYLESS_API_KEY,
);
}

function applyPersistedOAuthDefaults(
settings: Settings | null,
projected: Record<string, ProviderSettings>,
Expand Down Expand Up @@ -253,16 +264,14 @@ export function buildOpenAISource(fields: {
fields.reasoningEffort !== undefined
? { providerOptions: { reasoning_effort: fields.reasoningEffort } }
: {};
registerSourceSecret(fields.id, fields.apiKey);
return {
id: fields.id,
provider: "openai-compatible",
baseURL: isOllamaProviderId(fields.id)
? ollamaOpenAIBaseURL(fields.baseURL)
: normalizeOpenAICompatibleBaseURL(fields.baseURL),
apiKey:
fields.apiKey !== undefined && fields.apiKey.length > 0
? fields.apiKey
: KEYLESS_API_KEY,
credentialId: fields.id,
model: fields.model,
defaults: { maxTokens: SOURCE_MAX_TOKENS, ...overrides },
...(fields.quirks !== undefined ? { quirks: fields.quirks } : {}),
Expand Down Expand Up @@ -318,7 +327,8 @@ export type ProviderCatalogEntry = Omit<
// "codex-responses" adapter (the Codex backend speaks the Responses API, not
// Chat Completions) and carries the account id + a session id through
// providerOptions, where the adapter lifts them into request headers. The
// access token is the apiKey; the harness injects it as the bearer credential.
// access token is registered in the credential cell under the source id; the
// harness resolves it as the bearer credential at send time.
export function buildCodexSource(fields: {
id: string;
apiKey: string;
Expand All @@ -334,19 +344,21 @@ export function buildCodexSource(fields: {
providerOptions[CODEX_ACCOUNT_ID_OPTION] = fields.accountId;
if (fields.reasoningEffort !== undefined)
providerOptions["reasoning_effort"] = fields.reasoningEffort;
registerSourceSecret(fields.id, fields.apiKey);
return {
id: fields.id,
provider: CODEX_RESPONSES_PROVIDER,
baseURL: CODEX_BASE_URL,
apiKey: fields.apiKey,
credentialId: fields.id,
model: fields.model,
defaults: { maxTokens: SOURCE_MAX_TOKENS, providerOptions },
};
}

// Build the InferenceSource for an xAI/Grok OAuth profile. Routes to the
// "grok-responses" adapter (the grok-cli proxy speaks the Responses API, not
// Chat Completions). The access token is the apiKey; the caller's user id is
// Chat Completions). The access token is registered in the credential cell
// under the source id; the caller's user id is
// decoded from it and lifted into the x-grok-user-id header by the adapter.
// The session id becomes the request's prompt_cache_key so every call in the
// thread routes to the same cache shard (store:false has no other signal).
Expand All @@ -364,11 +376,12 @@ export function buildXaiSource(fields: {
if (userId !== undefined) providerOptions[GROK_USER_ID_OPTION] = userId;
if (fields.reasoningEffort !== undefined)
providerOptions["reasoning_effort"] = fields.reasoningEffort;
registerSourceSecret(fields.id, fields.apiKey);
return {
id: fields.id,
provider: GROK_RESPONSES_PROVIDER,
baseURL: XAI_BASE_URL,
apiKey: fields.apiKey,
credentialId: fields.id,
model: fields.model,
defaults: { maxTokens: SOURCE_MAX_TOKENS, providerOptions },
};
Expand All @@ -388,14 +401,12 @@ export function buildBifrostSource(fields: {
fields.reasoningEffort !== undefined
? { providerOptions: { reasoning_effort: fields.reasoningEffort } }
: {};
registerSourceSecret(fields.id, fields.apiKey);
return {
id: fields.id,
provider: BIFROST_PROVIDER,
baseURL: normalizeOpenAICompatibleBaseURL(fields.baseURL),
apiKey:
fields.apiKey !== undefined && fields.apiKey.length > 0
? fields.apiKey
: KEYLESS_API_KEY,
credentialId: fields.id,
model: fields.model,
defaults: { maxTokens: SOURCE_MAX_TOKENS, ...overrides },
};
Expand All @@ -408,14 +419,12 @@ export function buildAnthropicSource(fields: {
apiKey?: string;
model: string;
}): InferenceSource {
registerSourceSecret(fields.id, fields.apiKey);
return {
id: fields.id,
provider: "anthropic",
baseURL: fields.baseURL.replace(/\/+$/, ""),
apiKey:
fields.apiKey !== undefined && fields.apiKey.length > 0
? fields.apiKey
: KEYLESS_API_KEY,
credentialId: fields.id,
model: fields.model,
defaults: { maxTokens: SOURCE_MAX_TOKENS },
};
Expand All @@ -431,16 +440,13 @@ export function buildGoSource(fields: {
reasoningEffort?: ReasoningEffort;
}): InferenceSource {
const endpoint = resolveGoEndpoint(fields.model);
const apiKey =
fields.apiKey !== undefined && fields.apiKey.length > 0
? fields.apiKey
: KEYLESS_API_KEY;
registerSourceSecret(fields.id, fields.apiKey);
if (endpoint.adapter === "anthropic") {
return {
id: fields.id,
provider: OPENCODE_GO_MESSAGES_PROVIDER,
baseURL: endpoint.baseURL,
apiKey,
credentialId: fields.id,
model: fields.model,
defaults: {
maxTokens: SOURCE_MAX_TOKENS,
Expand All @@ -455,7 +461,7 @@ export function buildGoSource(fields: {
id: fields.id,
provider: OPENAI_RESPONSES_PROVIDER,
baseURL: endpoint.baseURL,
apiKey,
credentialId: fields.id,
model: fields.model,
defaults: {
maxTokens: SOURCE_MAX_TOKENS,
Expand All @@ -471,7 +477,7 @@ export function buildGoSource(fields: {
id: fields.id,
baseURL:
endpoint.baseURL.length > 0 ? endpoint.baseURL : OPENCODE_GO_BASE_URL,
apiKey,
...(fields.apiKey !== undefined ? { apiKey: fields.apiKey } : {}),
model: fields.model,
...(fields.reasoningEffort !== undefined
? { reasoningEffort: fields.reasoningEffort }
Expand Down Expand Up @@ -500,16 +506,13 @@ export function buildZenSource(fields: {
reasoningEffort?: ReasoningEffort;
}): InferenceSource {
const endpoint = resolveZenEndpoint(fields.model);
const apiKey =
fields.apiKey !== undefined && fields.apiKey.length > 0
? fields.apiKey
: KEYLESS_API_KEY;
registerSourceSecret(fields.id, fields.apiKey);
if (endpoint.adapter === "anthropic") {
return {
id: fields.id,
provider: ZEN_MESSAGES_PROVIDER,
baseURL: endpoint.baseURL,
apiKey,
credentialId: fields.id,
model: fields.model,
defaults: {
maxTokens: SOURCE_MAX_TOKENS,
Expand All @@ -524,7 +527,7 @@ export function buildZenSource(fields: {
id: fields.id,
provider: OPENAI_RESPONSES_PROVIDER,
baseURL: endpoint.baseURL,
apiKey,
credentialId: fields.id,
model: fields.model,
defaults: {
maxTokens: SOURCE_MAX_TOKENS,
Expand All @@ -540,7 +543,7 @@ export function buildZenSource(fields: {
id: fields.id,
baseURL:
endpoint.baseURL.length > 0 ? endpoint.baseURL : ZEN_DEFAULT_BASE_URL,
apiKey,
...(fields.apiKey !== undefined ? { apiKey: fields.apiKey } : {}),
model: fields.model,
...(fields.reasoningEffort !== undefined
? { reasoningEffort: fields.reasoningEffort }
Expand Down
2 changes: 2 additions & 0 deletions src/config/inference-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../provider/context-window.js";
import { createOpenAICompatibleAdapter } from "../provider/openai-compatible-adapter.js";
import { createInferenceDependencies } from "../provider/inference-dependencies.js";
import { clearSourceCredentials } from "./source-credentials.js";
import { OPENAI_RESPONSES_PROVIDER } from "../provider/openai-responses.js";
import { ZEN_MESSAGES_PROVIDER } from "../provider/zen-anthropic-adapter.js";
import { firstClassProviderById } from "../../packages/first-class-providers/src/index.js";
Expand Down Expand Up @@ -81,6 +82,7 @@ function settingsWithWindow(): Settings {
afterEach(() => {
setProviderContextWindowOverrides(undefined);
globalThis.fetch = originalFetch;
clearSourceCredentials();
});

describe("contextWindow / maxTokens split (CL-7784)", () => {
Expand Down
48 changes: 48 additions & 0 deletions src/config/source-credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// First-party credential cell backing the vendored inference auth model.
//
// Since the 1ad0104 re-vendor, `InferenceSource` carries no inline secret:
// it names a `credentialId` and every send resolves the secret through a
// `CredentialMaterialResolver` ("credential cell" in upstream terms — read
// `CredentialMaterialResolver`'s doc comment in the vendored
// `@intx/types`). This module is that cell for first-party API-key and
// OAuth access-token sources: each `buildXSource` in `./index.ts` registers
// the secret it was built with under the source id, and the inference entry
// points (`assemble-runtime`, subagent run, summarizer fallback) hand
// `readSourceCredentialMaterial` to the vendored trees as their resolver.
//
// Keyed by source id because ids are unique per live source within a
// process. The map lives at module scope so sources built in one layer
// (config) resolve in another (agent env, reactor options) without threading
// secrets through every intermediate shape.
import type { CredentialMaterialResolver } from "@intx/types";

const cell = new Map<string, string>();

export function registerSourceCredential(
credentialId: string,
secret: string,
): void {
cell.set(credentialId, secret);
}

/** The resolver handed to vendored inference calls. Fails closed. */
export const readSourceCredentialMaterial: CredentialMaterialResolver = (
credentialId: string,
) => {
const secret = cell.get(credentialId);
if (secret === undefined)
throw new Error(`Unknown inference credential "${credentialId}".`);
return { secret };
};

/** Non-throwing read for "did the token change?" comparisons. */
export function peekSourceCredentialSecret(
credentialId: string,
): string | undefined {
return cell.get(credentialId);
}

/** Test seam: empties the cell between cases. */
export function clearSourceCredentials(): void {
cell.clear();
}
2 changes: 1 addition & 1 deletion src/context-compactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
provider: "openai",
model: "test-model",
baseURL: "http://localhost:1",
apiKey: "k",
credentialId: "test",
};
let calls = 0;
const summarize = createModelSummarizer({
Expand Down
Loading
Loading