Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
46023bb
feat(providers): add focused Command Code hybrid gateway
jatmn Sep 2, 2026
16e6d48
fix(providers): reject Command Code placeholder keys and restore save…
jatmn Sep 2, 2026
3f41dc3
fix(providers): re-apply Command Code when the dedicated key drifts
jatmn Sep 2, 2026
d1adc7d
fix(providers): isolate Command Code from leftover OpenAI keys
jatmn Sep 2, 2026
a77a4c9
fix: enforce Command Code transport contract
jatmn Sep 2, 2026
6055bba
fix: apply model override before validation
jatmn Sep 2, 2026
97e9863
fix: support inline model overrides
jatmn Sep 2, 2026
6d84e06
fix: align early model parsing with CLI
jatmn Sep 2, 2026
679dd24
fix: address Command Code review feedback
jatmn Sep 2, 2026
306f60c
fix: validate Command Code profile models
jatmn Sep 2, 2026
66f7eca
fix: keep startup model display in sync
jatmn Sep 3, 2026
925d790
fix: avoid echoing rejected Command Code models
jatmn Sep 3, 2026
c8083d6
fix: share effective model flag parsing
jatmn Sep 3, 2026
abbc08b
fix: preserve teammate provider overrides
jatmn Sep 3, 2026
3f7729a
fix: isolate command code startup state
jatmn Sep 3, 2026
ad436ff
fix: align command code startup state
jatmn Sep 3, 2026
c8afff4
fix: close command code route gaps
jatmn Sep 3, 2026
494f548
fix: preserve teammate route precedence
jatmn Sep 3, 2026
79f68fe
fix: align launcher and root option parsing
jatmn Sep 3, 2026
383a99e
fix: preserve model and credential semantics
jatmn Sep 3, 2026
c7b930f
fix: reject Claude aliases on Command Code
jatmn Sep 3, 2026
ec8ab5d
fix: preserve root --model after a consumed delimiter
jatmn Sep 3, 2026
2e908cc
fix: reject Command Code Claude models in /model and lists
jatmn Sep 3, 2026
ceebfc4
fix: ignore nested --model when applying --provider
jatmn Sep 3, 2026
fdeb8b7
fix: unmount Ink if /model wait times out
jatmn Sep 3, 2026
f464f07
fix: enforce Command Code contracts at profile write time
jatmn Sep 3, 2026
c09ada1
fix: persist Command Code dedicated keys over leftover OpenAI env
jatmn Sep 3, 2026
2bb4333
fix: test background session root --model from launch argv
jatmn Sep 3, 2026
bb8a5ea
fix(commandcode): unify route validation contracts
jatmn Sep 3, 2026
d15014c
fix(commandcode): preserve explicit profile credentials
jatmn Sep 4, 2026
5ad20b9
fix(cli): preserve explicit provider routing
jatmn Sep 4, 2026
63ecb44
fix(cli): align provider option consumers
jatmn Sep 4, 2026
71404b7
fix(commandcode): align credential and discovery contracts
jatmn Sep 4, 2026
32acae0
docs(commandcode): clarify Claude model support
jatmn Sep 4, 2026
2ac51d2
fix(bg): align persisted provider metadata
jatmn Sep 4, 2026
604784f
fix(commandcode): preserve model and base defaults
jatmn Sep 4, 2026
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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,16 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# OPENAI_BASE_URL=https://llmtr.com/v1
# OPENAI_MODEL=deepseek/deepseek-v4-flash

# For Command Code, use its dedicated key. Claude models on this provider
# require the Anthropic /messages API and are not routed here. Select the
# route with /provider or --provider commandcode (or CLAUDE_CODE_USE_OPENAI=1);
# CMD_API_KEY alone does not autodetect Command Code:
# CMD_API_KEY=your-commandcode-key-here
# COMMANDCODE_API_KEY=your-commandcode-key-here # supported fallback
# COMMAND_CODE_API_KEY=your-commandcode-key-here # official Command Code client variable
# OPENAI_BASE_URL=https://api.commandcode.ai/provider/v1
# OPENAI_MODEL=deepseek/deepseek-v4-flash

# Use a custom OpenAI-compatible endpoint (optional — defaults to api.openai.com)
# OPENAI_BASE_URL=https://api.openai.com/v1
# Choose the OpenAI-compatible API surface (optional).
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ Advanced and source-build guides:
| AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog |
| Concentrate | `/provider` or `CONCENTRATE_API_KEY` | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash` and auto-discovers the chat model catalog |
| LLMTR | `/provider` or OpenAI-compatible env vars | Multi-model gateway at `https://llmtr.com/v1`; `/provider` and `--provider llmtr` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://llmtr.com/v1` and `OPENAI_MODEL`; accepts `LLMTR_API_KEY` or `OPENAI_API_KEY` after the route is selected and discovers tool-capable Chat Completions models from the public catalog |
| Command Code | `/provider` or OpenAI-compatible env vars | Hybrid OpenAI-compatible gateway at `https://api.commandcode.ai/provider/v1`; `/provider` and `--provider commandcode` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://api.commandcode.ai/provider/v1` and `OPENAI_MODEL`; requires `CMD_API_KEY`, `COMMANDCODE_API_KEY`, or the official `COMMAND_CODE_API_KEY` after the route is selected and discovers Chat Completions models from the public catalog. Claude models are unsupported by this Chat Completions integration |
| ApiSmart | `/provider` or `APISMART_API_KEY` | Uses `https://gw.apismart.ai/v1`, defaults to `DEEPSEEK_V4_FLASH`, and supports optional `APISMART_MODEL` plus authenticated model discovery |
| Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models |
| Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` |
Expand Down
74 changes: 73 additions & 1 deletion scripts/provider-launch.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, test } from 'bun:test'
import { hasUsableOpenAILaunchCredential } from './provider-launch.ts'
import {
getOpenAILaunchCredentialError,
hasUsableOpenAILaunchCredential,
} from './provider-launch.ts'

describe('provider-launch OpenAI credential validation', () => {
test('accepts valid OPENAI_API_KEYS before placeholder OPENAI_API_KEY fallback', () => {
Expand All @@ -19,4 +22,73 @@ describe('provider-launch OpenAI credential validation', () => {
} as NodeJS.ProcessEnv),
).toBe(false)
})

test('accepts a dedicated Command Code credential on its selected route', () => {
expect(
hasUsableOpenAILaunchCredential({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.commandcode.ai/provider/v1',
CMD_API_KEY: 'cmd-key',
} as NodeJS.ProcessEnv),
).toBe(true)
})

test('accepts the Command Code fallback credential on its selected route', () => {
expect(
hasUsableOpenAILaunchCredential({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.commandcode.ai/provider/v1',
COMMANDCODE_API_KEY: 'fallback-key',
} as NodeJS.ProcessEnv),
).toBe(true)
})

test('rejects a generic credential for the dedicated-only Command Code route', () => {
expect(
hasUsableOpenAILaunchCredential({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.commandcode.ai/provider/v1',
OPENAI_API_KEY: 'generic-key',
} as NodeJS.ProcessEnv),
).toBe(false)
})

test.each([
'CMD_API_KEY',
'COMMANDCODE_API_KEY',
'COMMAND_CODE_API_KEY',
] as const)(
'rejects a placeholder %s on the selected Command Code route',
credentialEnvVar => {
expect(
hasUsableOpenAILaunchCredential({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.commandcode.ai/provider/v1',
[credentialEnvVar]: 'SUA_CHAVE',
} as NodeJS.ProcessEnv),
).toBe(false)
},
)

test('reports Command Code credential guidance for its selected route', () => {
expect(
getOpenAILaunchCredentialError({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.commandcode.ai/provider/v1',
} as NodeJS.ProcessEnv),
).toBe(
'CMD_API_KEY is required for the Command Code route (fallbacks: COMMANDCODE_API_KEY or COMMAND_CODE_API_KEY) and cannot include placeholder values such as SUA_CHAVE. Set one of those environment variables, rerun the launcher, then use /provider to save the Command Code profile.',
)
})

test('preserves generic OpenAI credential guidance outside Command Code', () => {
expect(
getOpenAILaunchCredentialError({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://api.openai.com/v1',
} as NodeJS.ProcessEnv),
).toBe(
'OPENAI_API_KEYS or OPENAI_API_KEY is required for openai profile and cannot include SUA_CHAVE. Run: bun run profile:init -- --provider openai --api-key <key>',
)
})
})
29 changes: 24 additions & 5 deletions scripts/provider-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {
type ProfileFile,
type ProviderProfile,
} from '../src/utils/providerProfile.ts'
import {
getRouteCredentialValue,
resolveActiveRouteIdFromEnv,
} from '../src/integrations/routeMetadata.ts'
import {
getAtomicChatChatBaseUrl,
getOllamaChatBaseUrl,
Expand Down Expand Up @@ -156,7 +160,22 @@ function hasUsableGeminiLaunchAuth(env: NodeJS.ProcessEnv): boolean {
export function hasUsableOpenAILaunchCredential(
env: NodeJS.ProcessEnv,
): boolean {
return resolveOpenAICredentialEnvState(env).configured
return getOpenAILaunchCredentialError(env) === null
}

export function getOpenAILaunchCredentialError(
env: NodeJS.ProcessEnv,
): string | null {
if (resolveActiveRouteIdFromEnv(env) === 'commandcode') {
if (getRouteCredentialValue('commandcode', env)) {
return null
}
return 'CMD_API_KEY is required for the Command Code route (fallbacks: COMMANDCODE_API_KEY or COMMAND_CODE_API_KEY) and cannot include placeholder values such as SUA_CHAVE. Set one of those environment variables, rerun the launcher, then use /provider to save the Command Code profile.'
}
if (resolveOpenAICredentialEnvState(env).configured) {
return null
}
return 'OPENAI_API_KEYS or OPENAI_API_KEY is required for openai profile and cannot include SUA_CHAVE. Run: bun run profile:init -- --provider openai --api-key <key>'
}

async function main(): Promise<void> {
Expand Down Expand Up @@ -234,10 +253,10 @@ async function main(): Promise<void> {
process.exit(1)
}

if (profile === 'openai' && !hasUsableOpenAILaunchCredential(env)) {
console.error(
'OPENAI_API_KEYS or OPENAI_API_KEY is required for openai profile and cannot include SUA_CHAVE. Run: bun run profile:init -- --provider openai --api-key <key>',
)
const openAICredentialError =
profile === 'openai' ? getOpenAILaunchCredentialError(env) : null
if (openAICredentialError) {
console.error(openAICredentialError)
process.exit(1)
}

Expand Down
79 changes: 79 additions & 0 deletions src/cli/bg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
LOG_STREAM_CHUNK_SIZE,
parseBackgroundInvocation,
parseLogsInvocation,
resolveBackgroundSessionModel,
resolveBackgroundSessionProvider,
} from './bg.js'
import {
BACKGROUND_SESSION_ID_ENV,
Expand Down Expand Up @@ -107,6 +109,83 @@ async function withTempFile<T>(
}

describe('background session CLI parsing', () => {
it('records the final root provider selected by CLI startup', async () => {
const parsed = parseBackgroundInvocation([
'--provider',
'anthropic',
'--provider=commandcode',
'--bg',
'do the work',
])
const launch = await buildBackgroundSessionLaunch(
parsed.childArgs,
'00000000-0000-4000-8000-000000000001',
)

expect(resolveBackgroundSessionProvider(launch.childArgs)).toBe(
'commandcode',
)
})

it('does not record provider-looking text consumed by a root option', async () => {
const parsed = parseBackgroundInvocation([
'--system-prompt',
'--provider=commandcode',
'--bg',
'do the work',
])
const launch = await buildBackgroundSessionLaunch(
parsed.childArgs,
'00000000-0000-4000-8000-000000000001',
)

expect(resolveBackgroundSessionProvider(launch.childArgs)).toBeUndefined()
})

it('records the root session model when a nested command also has --model', async () => {
const generatedSessionId = '00000000-0000-4000-8000-000000000001'

for (const nested of [
['aimlapi', 'topup'],
['auto-mode', 'critique'],
] as const) {
const parsed = parseBackgroundInvocation([
'--model',
'deepseek/deepseek-v4-flash',
...nested,
'--model',
'anthropic/claude-sonnet-4-6',
'--bg',
'do the work',
])
const launch = await buildBackgroundSessionLaunch(
parsed.childArgs,
generatedSessionId,
)

expect(resolveBackgroundSessionModel(launch.childArgs)).toBe(
'deepseek/deepseek-v4-flash',
)
}
})

it('does not record a nested --model as the background session model', async () => {
const parsed = parseBackgroundInvocation([
'aimlapi',
'topup',
'--model',
'anthropic/claude-sonnet-4-6',
'--bg',
'do the work',
])
const launch = await buildBackgroundSessionLaunch(
parsed.childArgs,
'00000000-0000-4000-8000-000000000001',
)

expect(resolveBackgroundSessionModel(launch.childArgs)).toBeUndefined()
Comment thread
jatmn marked this conversation as resolved.
})

it('generates a fresh bounded lower-case hex marker from 32 random bytes', () => {
const first = generateBackgroundProcessMarker(size => {
expect(size).toBe(32)
Expand Down
33 changes: 29 additions & 4 deletions src/cli/bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ import { closeSync, openSync } from 'node:fs'
import { open, unlink } from 'node:fs/promises'
import { basename } from 'node:path'
import treeKill from 'tree-kill'
import { argsBeforeDelimiter } from '../utils/cliArgs.js'
import { hasPrintFlag } from '../utils/printFlag.js'
import {
argsBeforeDelimiter,
parseModelFlagValue,
} from '../utils/cliArgs.js'
import {
argsBeforeModelOwningSubcommand,
hasPrintFlag,
parseRootOptionValue,
} from '../utils/printFlag.js'
import { isProcessRunning } from '../utils/genericProcessUtils.js'
import {
assertBackgroundSessionNameAvailable,
Expand Down Expand Up @@ -341,6 +348,24 @@ function findSessionName(args: string[]): string | undefined {
return findFlagValue(args, '--name') ?? findFlagValue(args, '-n')
}

/**
* Background session metadata must record the same root `--model` that CLI
* startup applies. Nested `aimlapi topup` / `auto-mode critique` flags are not
* the session model.
*/
export function resolveBackgroundSessionModel(
childArgs: readonly string[],
): string | undefined {
return parseModelFlagValue(argsBeforeModelOwningSubcommand(childArgs))
}

/** Keep registry metadata aligned with the provider selected by CLI startup. */
export function resolveBackgroundSessionProvider(
childArgs: readonly string[],
): string | undefined {
return parseRootOptionValue(childArgs, '--provider')
}

function hasPrintMode(args: string[]): boolean {
return hasPrintFlag(args)
}
Expand Down Expand Up @@ -1145,8 +1170,8 @@ export async function handleBgFlag(args: string[]): Promise<void> {
pid: child.pid,
cwd: process.cwd(),
command,
provider: findFlagValue(childArgs, '--provider'),
model: findFlagValue(childArgs, '--model'),
provider: resolveBackgroundSessionProvider(childArgs),
model: resolveBackgroundSessionModel(childArgs),
sessionId,
processMarker,
stdoutLogPath: logPaths.stdoutLogPath,
Expand Down
Loading
Loading