Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export WIGOLO_LLM_PROVIDER=gemini
export GEMINI_API_KEY=<free-key> # grab one at aistudio.google.com/apikey — the free tier is plenty
```

Any provider works (`anthropic` · `openai` · `groq`), or stay fully local and keyless with `WIGOLO_LLM_PROVIDER=ollama` (or any OpenAI-compatible URL). Set it in your shell or your agent's MCP `env` block. Providers, models, and the keyless local-model ladder are in the [configuration guide](docs/configuration.md).
Any provider works (`anthropic` · `openai` · `groq` · `orcarouter`), or stay fully local and keyless with `WIGOLO_LLM_PROVIDER=ollama` (or any OpenAI-compatible URL). Set it in your shell or your agent's MCP `env` block. Providers, models, and the keyless local-model ladder are in the [configuration guide](docs/configuration.md).

## What your agent gets back

Expand Down Expand Up @@ -302,7 +302,7 @@ A clean install works out of the box. Three settings raise output quality:

```bash
# 1. Synthesis — the biggest lever (research / agent / search-answer write real prose)
export WIGOLO_LLM_PROVIDER=gemini # or anthropic / openai / groq / ollama (keyless)
export WIGOLO_LLM_PROVIDER=gemini # or anthropic / openai / groq / orcarouter / ollama (keyless)
export GEMINI_API_KEY=<your-key>

# 2. Wider retrieval funnel
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ Core tools never need an LLM. Configuring one adds answer synthesis (`format: "a

| Env var | Default | What it does |
| --- | --- | --- |
| `WIGOLO_LLM_PROVIDER` | unset | `anthropic`, `openai`, `gemini`, `groq`, or `ollama` (any local OpenAI-compatible server). |
| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` / `GROQ_API_KEY` | unset | Per-provider keys, read from env. |
| `WIGOLO_LLM_PROVIDER` | unset | `anthropic`, `openai`, `gemini`, `groq`, `orcarouter`, or `ollama` (any local OpenAI-compatible server). |
| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` / `GROQ_API_KEY` / `ORCAROUTER_API_KEY` | unset | Per-provider keys, read from env. |
Comment on lines +102 to +103

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document WIGOLO_LLM_MODEL_ORCAROUTER.

The provider table documents ORCAROUTER_API_KEY, but it does not document the provider-specific model override stated in this PR. Add WIGOLO_LLM_MODEL_ORCAROUTER and describe its precedence relative to WIGOLO_LLM_MODEL.

🤖 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 `@docs/configuration.md` around lines 102 - 103, Update the configuration
provider table to document WIGOLO_LLM_MODEL_ORCAROUTER, including that it
overrides WIGOLO_LLM_MODEL when the orcarouter provider is selected.

| `WIGOLO_LLM_API_KEY` | unset | Generic key slot (used by `init --provider=...`; stored in the OS keychain, never passed as a flag). |
| `WIGOLO_LLM_MODEL` | provider default | Override the model name. |
| `WIGOLO_LLM_BASE_URL` | `http://localhost:11434` | Custom base URL for the `ollama` provider — point it at any OpenAI-compatible endpoint. |
Expand Down
2 changes: 1 addition & 1 deletion src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function maybePrintOllamaHint(print: (line: string) => void): Promi
}
}

const KEYSTORE_PROVIDERS: readonly LLMProvider[] = ['anthropic', 'openai', 'gemini', 'groq'];
const KEYSTORE_PROVIDERS: readonly LLMProvider[] = ['anthropic', 'openai', 'gemini', 'groq', 'orcarouter'];

/**
* One-line hint printed on a `--no-warmup` init: nothing was pre-downloaded, so
Expand Down
2 changes: 1 addition & 1 deletion src/cli/tui/actions/verify-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ function buildDefaultExtractProbe(): () => Promise<CapabilityResult> {

function buildDefaultSynthesisProbe(): () => Promise<CapabilityResult> {
const SYNTHESIS_SKIP_DETAIL =
'no provider key configured — add a provider key via `wigolo config` (or set ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY) to enable synthesis';
'no provider key configured — add a provider key via `wigolo config` (or set ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY / ORCAROUTER_API_KEY) to enable synthesis';

return async (): Promise<CapabilityResult> => {
// Resolve a provider key through SP4's secure key store (keychain →
Expand Down
1 change: 1 addition & 0 deletions src/cli/tui/extras-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const PROVIDER_CHOICES: ReadonlyArray<{ name: string; value: LLMProvider }> = [
{ name: 'Anthropic (Claude)', value: 'anthropic' },
{ name: 'OpenAI (GPT)', value: 'openai' },
{ name: 'Google Gemini', value: 'gemini' },
{ name: 'OrcaRouter', value: 'orcarouter' },
];

// Index signature lets us pass an ExtrasChoices straight to saveInitConfig
Expand Down
2 changes: 1 addition & 1 deletion src/cli/tui/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function parseCommon(args: readonly string[], known: ReadonlySet<string>): Raw {
return raw;
}

const VALID_PROVIDERS = ['anthropic', 'openai', 'gemini', 'ollama'] as const;
const VALID_PROVIDERS = ['anthropic', 'openai', 'gemini', 'ollama', 'orcarouter'] as const;
const VALID_SEARCH_BACKENDS = ['core', 'searxng', 'hybrid'] as const;

function parseInitOnlyFlags(args: readonly string[]): { provider?: string; search?: string; interactive: boolean; wizard: boolean; warmup: boolean } {
Expand Down
5 changes: 5 additions & 0 deletions src/cli/tui/schema/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export const llmCategory: CategoryDef = {
{ value: 'anthropic', label: 'Anthropic (Claude)' },
{ value: 'openai', label: 'OpenAI (GPT)' },
{ value: 'gemini', label: 'Google Gemini' },
{
value: 'orcarouter',
label: 'OrcaRouter',
hint: 'OpenAI-compatible gateway with gateway-level, zero-trust security for AI agents',
},
{
value: 'ollama',
label: 'Ollama (local LLM server)',
Expand Down
2 changes: 2 additions & 0 deletions src/extraction/llm-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { callAnthropic } from '../integrations/cloud/llm/anthropic.js';
import { callOpenAI } from '../integrations/cloud/llm/openai.js';
import { callGemini } from '../integrations/cloud/llm/gemini.js';
import { callGroq } from '../integrations/cloud/llm/groq.js';
import { callOrcaRouter } from '../integrations/cloud/llm/orcarouter.js';
import {
ensureLLMCacheTable,
insertLLMCache,
Expand Down Expand Up @@ -43,6 +44,7 @@ const ADAPTERS: Record<
openai: callOpenAI,
gemini: callGemini,
groq: callGroq,
orcarouter: callOrcaRouter,

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

ast-grep outline src/integrations/cloud/llm/model-select.ts --items all
rg -n -C 4 'WIGOLO_LLM_MODEL_ORCAROUTER|modelOverride|callOrcaRouter|extractWithLLM' src tests

Repository: KnockOutEZ/wigolo

Length of output: 39669


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n src/integrations/cloud/llm/model-select.ts
sed -n '1,150p' src/extraction/llm-fallback.ts
sed -n '1,230p' tests/unit/extraction/llm-fallback.test.ts
sed -n '1,120p' src/integrations/cloud/llm/types.ts

Repository: KnockOutEZ/wigolo

Length of output: 13622


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

fallback = Path("src/extraction/llm-fallback.ts").read_text()
select = Path("src/integrations/cloud/llm/model-select.ts").read_text()
adapters = {
    name: Path(f"src/integrations/cloud/llm/{name}.ts").read_text()
    for name in ("anthropic", "openai", "gemini", "groq", "orcarouter")
}

call = re.search(
    r"result\s*=\s*await\s+ADAPTERS\[provider\]\((.*?)\n\s*\),\s*apiKey",
    fallback,
    re.S,
)
assert call, "adapter dispatch not found"
dispatch = call.group(1)
print("dispatch_has_modelOverride:", "modelOverride" in dispatch)
print("resolveModel_reads_orcarouter_env:",
      "orcarouter: 'WIGOLO_LLM_MODEL_ORCAROUTER'" in select)

for name, source in adapters.items():
    has_override = "opts.modelOverride" in source
    print(f"{name}_uses_modelOverride:", has_override)
    assert has_override, f"{name} does not consume modelOverride"

assert "WIGOLO_LLM_MODEL_ORCAROUTER" in select
assert "modelOverride" not in dispatch
PY

Repository: KnockOutEZ/wigolo

Length of output: 272


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '85,112p' src/extraction/llm-fallback.ts
rg -n 'const model = opts\.modelOverride|WIGOLO_LLM_MODEL_ORCAROUTER|resolveModel|ADAPTERS\[provider\]' \
  src/integrations/cloud/llm src/extraction/llm-fallback.ts

python3 - <<'PY'
from pathlib import Path

fallback = Path("src/extraction/llm-fallback.ts").read_text().splitlines()
start = next(i for i, line in enumerate(fallback) if "ADAPTERS[provider]" in line)
dispatch = "\n".join(fallback[start:start + 4])
print(dispatch)
print("dispatch_has_modelOverride:", "modelOverride" in dispatch)
assert "modelOverride" not in dispatch

select = Path("src/integrations/cloud/llm/model-select.ts").read_text()
assert "orcarouter: 'WIGOLO_LLM_MODEL_ORCAROUTER'" in select
print("resolveModel_reads_orcarouter_env: True")

for name in ("anthropic", "openai", "gemini", "groq", "orcarouter"):
    source = Path(f"src/integrations/cloud/llm/{name}.ts").read_text()
    assert "opts.modelOverride" in source
    print(f"{name}_uses_modelOverride: True")
PY

Repository: KnockOutEZ/wigolo

Length of output: 2272


Resolve and propagate the configured extraction model.

extractWithLLM calls adapters without modelOverride, so provider-specific model settings are ignored. Its ${provider}:default cache key also collides across configured models. Resolve the model before cache lookup, include it in the cache key, and pass it as modelOverride.

🤖 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/extraction/llm-fallback.ts` at line 47, Update extractWithLLM to resolve
the configured extraction model before the cache lookup, include the resolved
model in the cache key to distinguish provider configurations, and pass it to
the selected adapter through modelOverride. Preserve the existing provider
dispatch while ensuring every adapter call uses the resolved model.

};

export async function extractWithLLM(
Expand Down
4 changes: 2 additions & 2 deletions src/fetch/browser-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1449,15 +1449,15 @@ export class MultiBrowserPool {
});
}

/** True when a vision-capable cloud provider (anthropic/openai/gemini) is
/** True when a vision-capable cloud provider (anthropic/openai/gemini/orcarouter) is
* configured. Groq (weak vision) and the text-only custom backend don't
* count, so ai-solve is skipped cleanly when only those are present. */
private async visionProviderAvailable(): Promise<boolean> {
try {
const { selectProviderWithKeyStore } = await import('../integrations/cloud/llm/select.js');
const resolved = await selectProviderWithKeyStore(process.env, { dataDir: getConfig().dataDir });
if (!resolved) return false;
return resolved.provider === 'anthropic' || resolved.provider === 'openai' || resolved.provider === 'gemini';
return resolved.provider === 'anthropic' || resolved.provider === 'openai' || resolved.provider === 'gemini' || resolved.provider === 'orcarouter';
} catch {
return false;
}
Expand Down
2 changes: 2 additions & 0 deletions src/integrations/cloud/llm/model-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ const PROVIDER_DEFAULTS: Record<LLMProvider, string> = {
openai: 'gpt-4o-mini',
gemini: 'gemini-2.5-flash-lite',
groq: 'llama-3.3-70b-versatile',
orcarouter: 'orcarouter/auto',
};

const PROVIDER_ENV: Record<LLMProvider, string> = {
anthropic: 'WIGOLO_LLM_MODEL_ANTHROPIC',
openai: 'WIGOLO_LLM_MODEL_OPENAI',
gemini: 'WIGOLO_LLM_MODEL_GEMINI',
groq: 'WIGOLO_LLM_MODEL_GROQ',
orcarouter: 'WIGOLO_LLM_MODEL_ORCAROUTER',
};

export function resolveModel(
Expand Down
97 changes: 97 additions & 0 deletions src/integrations/cloud/llm/orcarouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import OpenAI from 'openai';
import type { LLMCallOpts, LLMExtractResult } from './types.js';
import { validateAgainstSchema, type ValidationError } from './validate.js';

const DEFAULT_MODEL = 'orcarouter/auto';
const BASE_URL = 'https://api.orcarouter.ai/v1';

export async function callOrcaRouter(
opts: LLMCallOpts,
apiKey: string,
): Promise<LLMExtractResult> {
const client = new OpenAI({ apiKey, baseURL: BASE_URL });
const model = opts.modelOverride ?? DEFAULT_MODEL;
const start = Date.now();

const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [
{ role: 'user', content: buildPrompt(opts.prompt, opts.jsonSchema) },
];

const first = await runOnce(client, model, messages, opts.signal);
let errors = validateAgainstSchema(first.values, opts.jsonSchema);
if (errors.length === 0) {
return done(first.values, first.responseModel ?? model, start);
}

// Retry once with validation errors fed back to the model.
messages.push({ role: 'assistant', content: first.raw });
messages.push({ role: 'user', content: retryPrompt(errors) });

const second = await runOnce(client, model, messages, opts.signal);
errors = validateAgainstSchema(second.values, opts.jsonSchema);
if (errors.length > 0) {
throw new Error(
`orcarouter: response failed schema validation after retry: ${formatErrors(errors)}`,
);
}
return done(second.values, second.responseModel ?? model, start);
}

interface CallOnceResult {
values: Record<string, unknown>;
raw: string;
responseModel: string | undefined;
}

async function runOnce(
client: OpenAI,
model: string,
messages: Array<{ role: 'user' | 'assistant'; content: string }>,
signal: AbortSignal | undefined,
): Promise<CallOnceResult> {
const response = await client.chat.completions.create(
{
model,
messages,
response_format: { type: 'json_object' },
},
{ signal },
);
const content = response.choices?.[0]?.message?.content;
if (!content) {
throw new Error('orcarouter: empty content in response');
}
let values: Record<string, unknown>;
try {
values = JSON.parse(content);
} catch (e) {
throw new Error(`orcarouter: invalid JSON in response: ${(e as Error).message}`);
}
return { values, raw: content, responseModel: response.model };
}

function buildPrompt(prompt: string, schema: Record<string, unknown>): string {
return `${prompt}\n\nReturn JSON matching this schema:\n${JSON.stringify(schema)}`;
}

function retryPrompt(errors: ValidationError[]): string {
return `Your previous response failed schema validation:\n${formatErrors(errors)}\nReturn corrected JSON only.`;
}

function formatErrors(errors: ValidationError[]): string {
return errors.map((e) => `${e.path}: ${e.message}`).join('; ');
}

function done(
values: Record<string, unknown>,
model: string,
start: number,
): LLMExtractResult {
return {
values,
provider: 'orcarouter',
model,
cached: false,
latencyMs: Date.now() - start,
};
}
2 changes: 1 addition & 1 deletion src/integrations/cloud/llm/run.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Unified entry point for LLM calls across wigolo. Selects a backend from
// env (or keystore) and delegates:
// - cloud provider name (anthropic/openai/gemini/groq) → SDK adapter
// - cloud provider name (anthropic/openai/gemini/groq/orcarouter) → SDK adapter
// - OpenAI-compatible URL (http://...) → POST /v1/chat/completions
//
// SP4: keys are resolved via resolveProviderKey (keychain → file → env) and
Expand Down
3 changes: 2 additions & 1 deletion src/integrations/cloud/llm/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import type { KeyStoreOpts } from '../../../security/key-store.js';
import { readPersistedConfig, defaultConfigPath } from '../../../persisted-config.js';
import { resolveCustomBackend } from './custom-backend.js';

const PROVIDER_ORDER: LLMProvider[] = ['anthropic', 'openai', 'gemini', 'groq'];
const PROVIDER_ORDER: LLMProvider[] = ['anthropic', 'openai', 'gemini', 'groq', 'orcarouter'];

const PROVIDER_ENV: Record<LLMProvider, string> = {
anthropic: 'ANTHROPIC_API_KEY',
openai: 'OPENAI_API_KEY',
gemini: 'GEMINI_API_KEY',
groq: 'GROQ_API_KEY',
orcarouter: 'ORCAROUTER_API_KEY',
};

// Extra env var names accepted for a provider's key, beyond the canonical one
Expand Down
34 changes: 34 additions & 0 deletions src/integrations/cloud/llm/text-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,39 @@ export async function callGroqText(opts: TextCallOpts, apiKey: string): Promise<
};
}

export async function callOrcaRouterText(opts: TextCallOpts, apiKey: string): Promise<TextCallResult> {
const { default: OpenAI } = await import('openai');
const client = new OpenAI({ apiKey, baseURL: 'https://api.orcarouter.ai/v1' });
const start = Date.now();
const content = opts.image
? [
{ type: 'text' as const, text: opts.prompt },
{
type: 'image_url' as const,
image_url: { url: `data:${opts.image.mediaType};base64,${opts.image.data}` },
},
]
: opts.prompt;
Comment on lines +168 to +180

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document OrcaRouter vision support in TextCallOpts.

callOrcaRouterText now accepts opts.image, but the TextCallOpts documentation at Line 27 still lists only anthropic/openai/gemini. Add orcarouter so the public adapter contract matches the implementation.

🤖 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/integrations/cloud/llm/text-adapters.ts` around lines 168 - 180, Update
the TextCallOpts documentation to include orcarouter among the supported
providers, keeping the public contract aligned with callOrcaRouterText’s
opts.image vision support.

const response = await client.chat.completions.create(
{
model: opts.model,
max_completion_tokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
messages: [{ role: 'user', content }],
},
{ signal: opts.signal },
);
const text = response.choices?.[0]?.message?.content;
if (typeof text !== 'string' || text.trim().length === 0) {
throw new Error('orcarouter: empty content in response');
}
return {
text,
provider: 'orcarouter',
model: response.model ?? opts.model,
latencyMs: Date.now() - start,
};
}

export const TEXT_ADAPTERS: Record<
LLMProvider,
(opts: TextCallOpts, apiKey: string) => Promise<TextCallResult>
Expand All @@ -173,4 +206,5 @@ export const TEXT_ADAPTERS: Record<
openai: callOpenAIText,
gemini: callGeminiText,
groq: callGroqText,
orcarouter: callOrcaRouterText,
};
2 changes: 1 addition & 1 deletion src/integrations/cloud/llm/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type LLMProvider = 'anthropic' | 'openai' | 'gemini' | 'groq';
export type LLMProvider = 'anthropic' | 'openai' | 'gemini' | 'groq' | 'orcarouter';

export interface LLMExtractResult {
values: Record<string, unknown>;
Expand Down
4 changes: 2 additions & 2 deletions src/security/key-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export const PICKER_PROVIDERS: ReadonlyArray<LLMProvider | 'custom'> = [
'custom',
];

// All providers that can have keystore entries (including groq via env only)
const STORE_PROVIDERS: ReadonlyArray<LLMProvider> = ['anthropic', 'openai', 'gemini', 'groq'];
// All providers that can have keystore entries (including groq/orcarouter via env only)
const STORE_PROVIDERS: ReadonlyArray<LLMProvider> = ['anthropic', 'openai', 'gemini', 'groq', 'orcarouter'];

/**
* Process-lifetime memo for resolveProviderKey, keyed by `provider:dataDir`.
Expand Down
20 changes: 19 additions & 1 deletion tests/integration/llm-fallback-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('llm-fallback e2e per provider', () => {
delete process.env.OPENAI_API_KEY;
delete process.env.GOOGLE_API_KEY;
delete process.env.GROQ_API_KEY;
delete process.env.ORCAROUTER_API_KEY;
delete process.env.WIGOLO_LLM_PROVIDER;
resetConfig();
initDatabase(':memory:');
Expand Down Expand Up @@ -134,7 +135,23 @@ describe('llm-fallback e2e per provider', () => {
expect(out.provider).toBe('groq');
});

it('no-key returns warning listing all four env vars', async () => {
it('orcarouter full path', async () => {
process.env.ORCAROUTER_API_KEY = 'k';
openaiCreate.mockResolvedValue({
choices: [{ message: { content: '{"price":"$5"}' } }],
model: 'orcarouter/auto',
});
const out = await extractWithLLM({
html: '<p/>',
jsonSchema: schema,
partial: {},
missing: ['price'],
});
expect(out.values).toEqual({ price: '$5' });
expect(out.provider).toBe('orcarouter');
});

it('no-key returns warning listing all five env vars', async () => {
const out = await extractWithLLM({
html: '<p/>',
jsonSchema: schema,
Expand All @@ -146,6 +163,7 @@ describe('llm-fallback e2e per provider', () => {
expect(w).toMatch(/OPENAI_API_KEY/);
expect(w).toMatch(/GEMINI_API_KEY/);
expect(w).toMatch(/GROQ_API_KEY/);
expect(w).toMatch(/ORCAROUTER_API_KEY/);
});

it('budget exhaustion blocks call', async () => {
Expand Down
1 change: 1 addition & 0 deletions tests/integration/zero-touch-just-works.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const RELEVANT_ENV_VARS = [
'GEMINI_API_KEY',
'GOOGLE_API_KEY',
'GROQ_API_KEY',
'ORCAROUTER_API_KEY',
'WIGOLO_DATA_DIR',
'WIGOLO_CONFIG_PATH',
];
Expand Down
1 change: 1 addition & 0 deletions tests/unit/agent/pipeline.llm-gate-keystore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const KEY_ENV_VARS = [
'OPENAI_API_KEY',
'GOOGLE_API_KEY',
'GROQ_API_KEY',
'ORCAROUTER_API_KEY',
'WIGOLO_LLM_PROVIDER',
'WIGOLO_LLM_API_KEY',
];
Expand Down
6 changes: 5 additions & 1 deletion tests/unit/cli/tui/actions/provider-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,15 @@ describe('provider actions (SP4)', () => {
});
});

describe('PICKER_PROVIDERS — groq hidden from picker (locked decision)', () => {
describe('PICKER_PROVIDERS — groq/orcarouter hidden from picker (locked decision)', () => {
it('does NOT include groq', () => {
expect(PICKER_PROVIDERS).not.toContain('groq');
});

it('does NOT include orcarouter', () => {
expect(PICKER_PROVIDERS).not.toContain('orcarouter');
});

it('includes the four picker-visible providers', () => {
expect(PICKER_PROVIDERS).toContain('anthropic');
expect(PICKER_PROVIDERS).toContain('openai');
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/cli/tui/schema/llm-no-custom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('llmCategory — no custom provider (Fix D)', () => {
const values = provider?.options?.map((o) => o.value) ?? [];
expect(values).not.toContain('custom');
// The keyed cloud providers remain; slice-3 appended the keyless 'ollama' lever.
expect(values).toEqual(['anthropic', 'openai', 'gemini', 'ollama']);
expect(values).toEqual(['anthropic', 'openai', 'gemini', 'orcarouter', 'ollama']);
});

it('llmCategory has exactly 2 fields after removing custom + endpoint URL field', () => {
Expand Down
Loading