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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 8 additions & 7 deletions gui/src/combo-workspace-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export type ComboEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";

export const COMBO_EFFORTS: ComboEffort[] = ["low", "medium", "high", "xhigh", "max", "ultra"];

/** Intersection of per-member effort ladders; unknown ladders contribute no selectable efforts. */
/**
* Intersection of advertised effort ladders for picker availability.
* Unknown ladders are wildcards here only; runtime injection remains fail-closed.
*/
export function intersectComboEfforts(
targets: readonly ComboTarget[],
modelEfforts: ReadonlyMap<string, readonly string[] | undefined>,
Expand All @@ -20,19 +23,17 @@ export function intersectComboEfforts(
for (const target of complete) {
const key = `${target.provider.trim()}/${target.model.trim()}`;
const listed = modelEfforts.get(key);
// Missing metadata must not invent a full ladder — runtime omits the combo default when
// supportedLadderFor is undefined (#488 / Codex review).
const member: string[] = listed === undefined
? []
: listed.filter((effort) => effortSet.has(effort));
if (listed === undefined) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the effort-picker hint for wildcard ladders

When a selected target has no effort metadata, this branch now preserves the other targets' efforts—or offers every effort when all ladders are unknown—but gui/src/i18n/en.ts still tells users that targets without metadata “offer none.” The visible explanation therefore contradicts the picker; revise the hint consistently across the locale files to explain the wildcard behavior.

AGENTS.md reference: gui/AGENTS.md:L14-L18

Useful? React with 👍 / 👎.

const member = listed.filter((effort) => effortSet.has(effort));
if (common === null) {
common = member;
} else {
const memberSet = new Set(member);
common = common.filter((effort) => memberSet.has(effort));
}
}
const commonSet = new Set(common ?? []);
if (common === null) return [...COMBO_EFFORTS];
const commonSet = new Set(common);
return COMBO_EFFORTS.filter((effort) => commonSet.has(effort));
}

Expand Down
31 changes: 12 additions & 19 deletions src/clients/config-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,9 @@ export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY";
/** Env reference shared by apiKey and the dedicated proxy admission header. */
export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`;

/** Env var Pi interpolates. Pi takes bare `$NAME`, not opencode's `{env:NAME}`. */
export const PI_API_KEY_ENV = "OPENCODEX_API_KEY";

/** Pi's reference form for the admission key. Never the value. */
export const PI_API_KEY_ENV_REF = `$${PI_API_KEY_ENV}`;

/**
* Hermes interpolates `${VAR}` anywhere in config.yaml, so the credential stays
* in the environment exactly as it does for OpenCode and Pi.
* in the environment exactly as it does for OpenCode.
*/
export const HERMES_API_KEY_ENV = "OPENCODEX_HERMES_API_KEY";
export const HERMES_API_KEY_ENV_REF = `\${${HERMES_API_KEY_ENV}}`;
Expand All @@ -125,12 +119,12 @@ export const OPENCLAW_API_KEY_ENV = "OPENCODEX_OPENCLAW_API_KEY";
export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`;

/**
* Kimi Code reads credentials ONLY from its config file — it never falls back
* to the shell environment. A loopback bind needs no real admission key, so we
* emit the same placeholder the Grok managed block uses rather than a user
* secret; a non-loopback bind is refused by the writer instead of papered over.
* Placeholder credential for loopback-only clients (Kimi, Pi). A loopback
* bind needs no real admission key, so we emit the same placeholder the Grok
* managed block uses rather than a user secret. Pi resolves `apiKey` before
* building its model list and hides the provider when an env reference is unset.
*/
export const KIMI_LOOPBACK_PLACEHOLDER = "opencodex-loopback";
export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback";

/**
* Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey`
Expand Down Expand Up @@ -728,7 +722,7 @@ function buildPiClientConfig(ctx: ExportContext): PiGeneratedConfig {
[OPENCODE_PROVIDER_ID]: {
baseUrl: ctx.baseUrl,
api: PI_API_DIALECT,
apiKey: PI_API_KEY_ENV_REF,
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update Pi credential guidance for the placeholder

After Pi starts serializing this literal placeholder, the shipped guidance still documents the removed environment-reference contract: src/cli/help.ts:181 says only Kimi uses a placeholder, while docs-site/src/content/docs/reference/cli/agents.md:176-182 and docs-site/src/content/docs/guides/integrations.md:12,85-87 still direct Pi users to OPENCODEX_API_KEY. Update those user-facing instructions and translated documentation so exported behavior and setup guidance agree.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid sending Pi placeholder as native Direct bearer

When Pi uses one of the exported native bare OpenAI rows while the canonical openai provider is configured with codexAccountMode: "direct", Pi sends this literal apiKey as its Authorization bearer; the chat-completions bridge forwards Authorization on direct native routes instead of replacing it with the main account token, so the ChatGPT upstream receives Bearer opencodex-loopback and those native Pi calls fail even on loopback. Keep the placeholder from becoming an upstream bearer for native/direct routes, or omit native/direct rows from Pi exports.

AGENTS.md reference: src/AGENTS.md:L20-L20

Useful? React with 👍 / 👎.

models,
},
},
Expand Down Expand Up @@ -808,7 +802,7 @@ function buildKimiClientConfig(ctx: ExportContext): KimiGeneratedConfig {
[OPENCODE_PROVIDER_ID]: {
type: "openai",
base_url: ctx.baseUrl,
api_key: KIMI_LOOPBACK_PLACEHOLDER,
api_key: LOOPBACK_API_KEY_PLACEHOLDER,
},
},
models,
Expand Down Expand Up @@ -949,15 +943,14 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
id: "pi",
filename: "pi-models.json",
destination: () => join(homedir(), ".pi", "agent", "models.json"),
apiKeyEnv: PI_API_KEY_ENV,
exportHint: `export ${PI_API_KEY_ENV}=<your key>`,
apiKeyEnv: "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Suppress key warnings for placeholder-only Pi configs

When the dashboard has no generated API keys, ClientConfigDialog unconditionally renders api.clientConfig.noKeyYet and interpolates envelope.apiKeyEnv; returning an empty string here makes the Pi dialog display a blank key name and instruct users to generate a key for off-loopback use, even though Pi is marked loopback-only and now needs no key. Gate that warning on a nonempty credential requirement, or represent placeholder-only clients explicitly.

Useful? React with 👍 / 👎.

exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.",
build: buildPiClientConfig,
format: "json",
summarize: summarizePi,
buildContribution: buildPiContribution,
// No header field in Pi's provider block (and the schema is unverified
// against a real install), so there is nowhere to put the dedicated
// admission header a remote bind requires.
// No header field in Pi's provider block, so there is nowhere to put the
// dedicated admission header a remote bind requires.
loopbackOnly: true,
Comment on lines +946 to 954

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

ast-grep outline src/clients/config-export.ts --items all

rg -n -C 10 --glob '*.ts' \
  'buildClientConfig|buildPiClientConfig|EXPORT_CLIENTS|apiKeyEnv|isLoopback|loopback|admission|baseUrl' \
  src tests

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Inspect the export spec definitions and management route usage without exceeding output limits.
wc -l src/clients/config-export.ts src/server/management/model-routes.ts src/routes.ts src/server/routes.ts src/server/.*

printf '\n--- config-export outline relevant symbols ---\n'
ast-grep outline src/clients/config-export.ts | sed -n '1,220p'

printf '\n--- relevant config-export sections ---\n'
sed -n '700,750p' src/clients/config-export.ts
sed -n '920,970p' src/clients/config-export.ts
rg -n --glob '*.ts' -C 3 'export type |api-spec|EXPORT_CLIENTS|buildClientConfig|exportClient|spec\|specs|client:\s*\{' src/clients/config-export.ts

printf '\n--- management route relevant sections ---\n'
sed -n '150,210p' src/server/management/model-routes.ts
rg -n --glob '*.ts' -C 4 'buildClientConfigText|model-routes|specs|client' src/server/management/model-routes.ts

Repository: lidge-jun/opencodex

Length of output: 392


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '--- relevant config-export sections ---\n'
sed -n '1,180p' src/clients/config-export.ts
sed -n '680,760p' src/clients/config-export.ts
sed -n '910,990p' src/clients/config-export.ts

printf '\n--- targeted config-export refs ---\n'
rg -n --glob '*.ts' -C 3 'buildClientConfig|buildClientConfigText|buildPiClientConfig|EXPORT_CLIENTS|exportClient|specs|client:' src tests 2>/dev/null | sed -n '1,260p'

printf '\n--- management route relevant sections ---\n'
sed -n '1,230p' src/server/management/model-routes.ts

printf '\n--- routing files ---\n'
git ls-files src | rg 'route|management|config' | sed -n '1,200p'

printf '\n--- targeted route/management refs ---\n'
rg -n --glob '*.ts' -C 4 'config-export|model-routes|model-config|export|client' src/server src/routes tests 2>/dev/null | sed -n '1,340p'

Repository: lidge-jun/opencodex

Length of output: 248


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- relevant config-export sections ---'
sed -n '1,180p' src/clients/config-export.ts
sed -n '680,760p' src/clients/config-export.ts
sed -n '910,990p' src/clients/config-export.ts

printf '%s\n' '--- targeted config-export refs ---'
rg -n --glob '*.ts' -C 3 'buildClientConfig|buildClientConfigText|buildPiClientConfig|EXPORT_CLIENTS|exportClient|specs|client:' src tests 2>/dev/null | sed -n '1,260p'

printf '%s\n' '--- management route relevant sections ---'
sed -n '1,230p' src/server/management/model-routes.ts

printf '%s\n' '--- routing files ---'
git ls-files src | rg 'route|management|config' | sed -n '1,200p'

printf '%s\n' '--- targeted route/management refs ---'
rg -n --glob '*.ts' -C 4 'config-export|model-routes|model-config|export|client' src/server src/routes tests 2>/dev/null | sed -n '1,340p'

Repository: lidge-jun/opencodex

Length of output: 50375


Block non-loopback Pi exports from the shared exporter.

In src/server/management/model-routes.ts:183-186, /api/client-config builds pi with the live proxy opencodeProxyBaseUrl(...), but pi-loopback is not a loopback host unless the API port is 10100. src/clients/config-export.ts:952-954 already marks pi.loopbackOnly: true, with no field to carry x-opencodex-api-key. Reject buildClientConfigText("pi", ...) whenever src/clients/config-export.ts:701 (ctx.baseUrl) resolves to a remote target, then cover the rejection in the existing export/management route tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/clients/config-export.ts` around lines 946 - 954, Update
buildClientConfigText to enforce the loopbackOnly flag for the Pi provider: when
the pi export’s ctx.baseUrl resolves to a non-loopback target, reject the export
instead of generating configuration. Preserve valid loopback exports, including
pi-loopback, and add coverage in the existing client-config export and
management route tests.

Source: Path instructions

},
hermes: {
Expand Down
2 changes: 2 additions & 0 deletions src/combos/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export function concreteComboRequestBody(
&& !Object.prototype.hasOwnProperty.call(reasoning, "effort")
);
if (!needsDefault) return clone;
// Picker availability treats an unknown ladder as a wildcard, but runtime
// injection stays fail-closed until this concrete target advertises support.
if (!targetReasoningEfforts?.includes(defaultEffort)) {
const key = `${target.provider}/${target.model}:${defaultEffort}`;
if (!warnedUnsupportedDefaults.has(key)) {
Expand Down
14 changes: 10 additions & 4 deletions tests/cli-export-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,15 @@ describe("ocx export human output (accept criterion 2)", () => {
expect(result.stdout).toContain("3 models; 1 omit context limits");
});

test("Pi names its own destination and env var", async () => {
test("Pi names its own destination and needs no env var", async () => {
const proxy = fakeProxy();
const result = await run(["--client", "pi"], { baseUrl: proxy.baseUrl });
expect(result.stdout).toContain(join(".pi", "agent", "models.json"));
expect(result.stdout).toContain("export OPENCODEX_API_KEY=");
// Pi resolves `apiKey` before building its model list and hides the provider
// when an env reference is unset, so a loopback bind ships the non-secret
// placeholder instead of an env var the user was never told to export.
expect(result.stdout).toContain("opencodex-loopback");
expect(result.stdout).not.toContain("export OPENCODEX_API_KEY=");
});
});

Expand Down Expand Up @@ -297,8 +301,10 @@ describe("ocx export never serializes a key (accept criterion 6)", () => {
for (const [args, envRef] of [
[["--client", "opencode"], "{env:OPENCODEX_OPENCODE_API_KEY}"],
[["--client", "opencode", "--json"], "{env:OPENCODEX_OPENCODE_API_KEY}"],
[["--client", "pi"], "$OPENCODEX_API_KEY"],
[["--client", "pi", "--json"], "$OPENCODEX_API_KEY"],
// Pi ships the non-secret loopback placeholder rather than an env reference;
// the property under test is unchanged — no real key ever reaches stdout.
[["--client", "pi"], "opencodex-loopback"],
[["--client", "pi", "--json"], "opencodex-loopback"],
] as Array<[string[], string]>) {
logs = [];
errors = [];
Expand Down
4 changes: 2 additions & 2 deletions tests/client-config-export-new-clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
EXPORT_CLIENT_IDS,
GAJAE_API_KEY_ENV,
HERMES_API_KEY_ENV_REF,
KIMI_LOOPBACK_PLACEHOLDER,
LOOPBACK_API_KEY_PLACEHOLDER,
OPENCLAW_API_KEY_ENV_REF,
OPENCODE_PROVIDER_ID,
buildClientConfig,
Expand Down Expand Up @@ -235,7 +235,7 @@ describe("kimi", () => {

test("uses the loopback placeholder because Kimi reads no environment", () => {
const doc = buildClientConfig("kimi", ctx()) as KimiGeneratedConfig;
expect(doc.providers[OPENCODE_PROVIDER_ID]!.api_key).toBe(KIMI_LOOPBACK_PLACEHOLDER);
expect(doc.providers[OPENCODE_PROVIDER_ID]!.api_key).toBe(LOOPBACK_API_KEY_PLACEHOLDER);
});

test("never emits capabilities it cannot assert", () => {
Expand Down
16 changes: 7 additions & 9 deletions tests/client-config-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import {
EXPORT_CLIENT_IDS,
OPENCODE_API_KEY_ENV,
OPENCODE_API_KEY_ENV_REF,
PI_API_KEY_ENV,
PI_API_KEY_ENV_REF,
LOOPBACK_API_KEY_PLACEHOLDER,
SCHEMA_REQUIRED_OUTPUT_BUDGET,
buildClientConfig,
buildClientConfigText,
Expand Down Expand Up @@ -160,12 +159,11 @@ describe("Pi serializer (accept criterion 2)", () => {
]);
});

test("provider envelope names the OpenAI-compatible dialect and the env reference", () => {
test("provider envelope names the OpenAI-compatible dialect and the loopback placeholder", () => {
const provider = piConfig().providers.opencodex!;
expect(provider.baseUrl).toBe(BASE_URL);
expect(provider.api).toBe("openai-completions");
expect(provider.apiKey).toBe(PI_API_KEY_ENV_REF);
expect(provider.apiKey).toBe("$OPENCODEX_API_KEY");
expect(provider.apiKey).toBe(LOOPBACK_API_KEY_PLACEHOLDER);
});

test("cost is omitted on every entry — zeros would assert routed models are free", () => {
Expand Down Expand Up @@ -236,7 +234,7 @@ describe("no credential ever reaches the output (accept criterion 3)", () => {

test("each client emits only its own documented env reference", () => {
expect(JSON.stringify(opencodeConfig())).toContain(OPENCODE_API_KEY_ENV_REF);
expect(JSON.stringify(piConfig())).toContain(PI_API_KEY_ENV_REF);
expect(JSON.stringify(piConfig())).toContain(LOOPBACK_API_KEY_PLACEHOLDER);
expect(JSON.stringify(piConfig())).not.toContain("{env:");
});
});
Expand Down Expand Up @@ -348,7 +346,7 @@ describe("EXPORT_CLIENTS registry", () => {
"opencodex": {
"baseUrl": "http://127.0.0.1:10100/v1",
"api": "openai-completions",
"apiKey": "$OPENCODEX_API_KEY",
"apiKey": "opencodex-loopback",
"models": [
{
"id": "anthropic/claude-opus-5",
Expand Down Expand Up @@ -449,8 +447,8 @@ describe("EXPORT_CLIENTS registry", () => {
test("apiKeyEnv and exportHint name the variable the config references", () => {
expect(EXPORT_CLIENTS.opencode.apiKeyEnv).toBe(OPENCODE_API_KEY_ENV);
expect(EXPORT_CLIENTS.opencode.exportHint).toContain(OPENCODE_API_KEY_ENV);
expect(EXPORT_CLIENTS.pi.apiKeyEnv).toBe(PI_API_KEY_ENV);
expect(EXPORT_CLIENTS.pi.exportHint).toContain(PI_API_KEY_ENV);
expect(EXPORT_CLIENTS.pi.apiKeyEnv).toBe("");
expect(EXPORT_CLIENTS.pi.exportHint).toContain("loopback");
for (const id of EXPORT_CLIENT_IDS) {
expect(EXPORT_CLIENTS[id].exportHint).not.toContain("ocx_");
}
Expand Down
4 changes: 2 additions & 2 deletions tests/client-config-new-clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
EXPORT_CLIENT_IDS,
GAJAE_API_KEY_ENV,
HERMES_API_KEY_ENV_REF,
KIMI_LOOPBACK_PLACEHOLDER,
LOOPBACK_API_KEY_PLACEHOLDER,
OPENCLAW_API_KEY_ENV_REF,
OPENCODE_PROVIDER_ID,
buildClientConfig,
Expand Down Expand Up @@ -56,7 +56,7 @@ describe("no client config ever carries a credential", () => {

test("kimi uses the loopback placeholder because it cannot read env vars", () => {
const doc = buildClientConfig("kimi", ctx()) as KimiGeneratedConfig;
expect(doc.providers[OPENCODE_PROVIDER_ID]!.api_key).toBe(KIMI_LOOPBACK_PLACEHOLDER);
expect(doc.providers[OPENCODE_PROVIDER_ID]!.api_key).toBe(LOOPBACK_API_KEY_PLACEHOLDER);
});

test("gajae uses apiKeyEnv, not the apiKey footgun", () => {
Expand Down
18 changes: 17 additions & 1 deletion tests/combo-workspace-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import {
type ComboItem,
COMBO_EFFORTS,
buildComboAttention,
comboPublicModelId,
draftEquals,
Expand Down Expand Up @@ -171,13 +172,28 @@ describe("combo-workspace-data", () => {
)).toEqual(["medium", "high"]);
});

test("intersectComboEfforts treats unknown members as having no selectable efforts", () => {
test("intersectComboEfforts treats unknown members as picker wildcards", () => {
const map = new Map<string, readonly string[] | undefined>([
["a/m1", ["low", "medium"]],
]);
expect(intersectComboEfforts(
[{ provider: "a", model: "m1" }, { provider: "b", model: "unknown" }],
map,
)).toEqual(["low", "medium"]);
expect(intersectComboEfforts(
[{ provider: "b", model: "unknown" }],
map,
)).toEqual(COMBO_EFFORTS);
});

test("intersectComboEfforts keeps an advertised empty ladder restrictive", () => {
const map = new Map<string, readonly string[] | undefined>([
["a/m1", ["low", "medium"]],
["b/no-reasoning", []],
]);
expect(intersectComboEfforts(
[{ provider: "a", model: "m1" }, { provider: "b", model: "no-reasoning" }],
map,
)).toEqual([]);
});

Expand Down
9 changes: 8 additions & 1 deletion tests/combos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ describe("combo request cloning", () => {
expect(concreteComboRequestBody({ model: "combo/x" }, target, "high", ["low", "medium"]).reasoning).toBeUndefined();
});

test("debug-warns once per unsupported combo default", () => {
test("debug-warns once per unsupported or unknown combo default", () => {
const debug = spyOn(console, "debug").mockImplementation(() => {});
concreteComboRequestBody({ model: "combo/x" }, target, "high", []);
concreteComboRequestBody({ model: "combo/x" }, target, "high", []);
Expand All @@ -246,6 +246,13 @@ describe("combo request cloning", () => {
requestedEffort: "high",
capability: "unsupported",
});
concreteComboRequestBody({ model: "combo/x" }, target, "medium", undefined);
concreteComboRequestBody({ model: "combo/x" }, target, "medium", undefined);
expect(debug).toHaveBeenCalledTimes(2);
expect(debug.mock.calls[1]?.[1]).toMatchObject({
requestedEffort: "medium",
capability: "unknown",
});
debug.mockRestore();
});
});
Expand Down
7 changes: 3 additions & 4 deletions tests/management-client-config-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {
OPENCODE_API_KEY_ENV,
OPENCODE_CONFIG_SCHEMA,
OPENCODE_PROVIDER_ID,
PI_API_KEY_ENV,
PI_API_KEY_ENV_REF,
LOOPBACK_API_KEY_PLACEHOLDER,
buildClientConfig,
normalizeExportModels,
opencodeGlobalConfigPath,
Expand Down Expand Up @@ -149,11 +148,11 @@ describe("GET /api/client-config", () => {

expect(body.client).toBe("pi");
expect(body.filename).toBe("pi-models.json");
expect(body.apiKeyEnv).toBe(PI_API_KEY_ENV);
expect(body.apiKeyEnv).toBe("");

const provider = (body.config as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID];
expect(Array.isArray(provider.models)).toBe(true);
expect(provider.apiKey).toBe(PI_API_KEY_ENV_REF);
expect(provider.apiKey).toBe(LOOPBACK_API_KEY_PLACEHOLDER);
expect(provider.baseUrl).toBe("http://127.0.0.1:10100/v1");
expect(provider.models.map(model => model.id)).toContain("a/m1");
}, 15_000);
Expand Down
Loading