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
2 changes: 2 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ These are consumed via `getEnvApiKey()` (`packages/ai/src/stream.ts`) unless not
| `AI_GATEWAY_API_KEY` | Vercel AI Gateway auth | Using `vercel-ai-gateway` provider | |
| `CLOUDFLARE_AI_GATEWAY_API_KEY` | Cloudflare AI Gateway auth | Using `cloudflare-ai-gateway` provider | Base URL must be configured as `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic` |
| `ALIBABA_TOKEN_PLAN_API_KEY` | Alibaba Token Plan auth | Using `alibaba-token-plan` provider | |
| `CLINE_API_KEY` | Cline API / ClinePass auth | Using the `cline-pass` provider preset | Create under Settings > API Keys in the Cline dashboard |
| `CMD_API_KEY` | Command Code Provider API auth | Using the `commandcode-goat` provider preset | The GOAT coding plan may use this API according to its plan entitlement |
| `DEEPSEEK_API_KEY` | DeepSeek auth | Using DeepSeek models | |
| `KILO_API_KEY` | Kilo auth | Using Kilo models | |
| `OLLAMA_CLOUD_API_KEY` | Ollama Cloud auth | Using `ollama-cloud` provider | |
Expand Down
10 changes: 7 additions & 3 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,17 @@ providers:
- id: anthropic.claude-3-5-sonnet-20241022-v2:0
```

### MiniMax and GLM custom provider examples
### Coding-plan provider presets

For common MiniMax and GLM/zAI setup, prefer the provider presets so the OpenAI-compatible API, base URL, env var, model id, and compatibility flags are written together:
For supported coding-plan providers, prefer presets so the API type, base URL, environment variable, model catalog, discovery behavior, and compatibility flags are written together:

```sh
gjc setup provider --preset minimax
gjc setup provider --preset minimax-cn
gjc setup provider --preset glm
gjc setup provider --preset alibaba-token-plan
gjc setup provider --preset cline-pass
gjc setup provider --preset commandcode-goat
```

The same presets are available inside the TUI:
Expand All @@ -180,9 +182,11 @@ The same presets are available inside the TUI:
/provider add --preset glm
/provider add zai
/provider add --preset alibaba-token-plan
/provider add --preset cline-pass
/provider add --preset commandcode-goat
```

Presets only write `models.yml` entries that reference documented environment variable names (`MINIMAX_CODE_API_KEY`, `MINIMAX_CODE_CN_API_KEY`, `ZAI_API_KEY`, or `ALIBABA_TOKEN_PLAN_API_KEY`); they do not store or validate real credentials. The GLM preset aliases (`glm`, `zai`, `z-ai`) write an OpenAI-compatible custom provider named `glm-proxy` and do not replace the first-class `zai` provider. The Alibaba Token Plan preset (aliases: alibaba, token-plan) writes an OpenAI-compatible custom provider named alibaba-token-plan with per-model API routing (qwen3.8-max-preview uses openai-responses; glm-5.2, deepseek-v4-pro, and deepseek-v4-flash-0731 use openai-completions).
Presets only write `models.yml` entries that reference documented environment variable names (`MINIMAX_CODE_API_KEY`, `MINIMAX_CODE_CN_API_KEY`, `ZAI_API_KEY`, `ALIBABA_TOKEN_PLAN_API_KEY`, `CLINE_API_KEY`, or `CMD_API_KEY`); they do not store or validate real credentials. The GLM preset aliases (`glm`, `zai`, `z-ai`) write an OpenAI-compatible custom provider named `glm-proxy` and do not replace the first-class `zai` provider. The Alibaba Token Plan preset (aliases: `alibaba`, `token-plan`) writes an OpenAI-compatible custom provider named `alibaba-token-plan` with per-model API routing. The ClinePass preset (aliases: `clinepass`, `cline`) does not hardcode models: Cline's inference API has no working `/models` route, so GJC follows Cline's own catalog-generation source and fetches the live `cline-pass` provider catalog from `https://models.dev/api.json`. The Command Code GOAT preset (aliases: `commandcode`, `command-code`, `goat`) fetches its live `/provider/v1/models` catalog, routes every current or future `claude-*` model through Anthropic Messages, and routes other models through Chat Completions. Create the corresponding API key in the provider dashboard before inference; plan entitlement is enforced by the provider.

## Model profiles (`--mpreset`)

Expand Down
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Added first-class `cline-pass` and `commandcode-goat` provider presets with documented API endpoints, environment-variable credentials, non-hardcoded live model discovery from models.dev and the Command Code Provider API, and prefix-based Claude routing.

## [0.12.15] - 2026-08-06

## [0.12.14] - 2026-08-06
Expand Down
6 changes: 4 additions & 2 deletions packages/coding-agent/src/cli/setup-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function rejectProviderFlagsOutsideProvider(component: SetupComponent, flags: Se
console.error(chalk.red("Provider setup flags require the explicit `provider` component."));
console.error(
chalk.dim(
`Run: ${APP_NAME} setup provider --preset <minimax|glm> or ${APP_NAME} setup provider --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <id>`,
`Run: ${APP_NAME} setup provider --preset <id> (see --help for presets) or ${APP_NAME} setup provider --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <id>`,
),
);
process.exit(1);
Expand Down Expand Up @@ -731,6 +731,8 @@ ${chalk.bold("Components:")}
${chalk.bold("Provider example:")}
${APP_NAME} setup provider --preset minimax
${APP_NAME} setup provider --preset glm
${APP_NAME} setup provider --preset cline-pass
${APP_NAME} setup provider --preset commandcode-goat
MY_PROVIDER_KEY=sk-... ${APP_NAME} setup provider --compat openai --provider my-oai --base-url https://api.example.com/v1 --api-key-env MY_PROVIDER_KEY --model gpt-example

${chalk.bold("Hermes example:")}
Expand All @@ -743,7 +745,7 @@ ${chalk.bold("Options:")}
-c, --check Check if dependencies are installed without installing
-f, --force Overwrite existing default workflow skill files
--json Output status as JSON
--preset Provider preset: minimax, minimax-cn, or glm (aliases include minimax-code and zai)
--preset Provider preset id (run setup provider --help to list available presets)
--compat Provider compatibility: openai or anthropic
--provider Provider id to add to models.yml
--base-url Provider API base URL
Expand Down
107 changes: 88 additions & 19 deletions packages/coding-agent/src/config/model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2154,7 +2154,8 @@ export class ModelRegistry {
: strategy;
const mergeInput = await this.#discoveryManager.discover(effectiveProviderConfig, refreshStrategy, {
cacheDbPath: this.#cacheDbPath,
requiresAuth: provider => !this.#isCredentiallessProvider(provider.provider),
requiresAuth: provider =>
provider.discovery.type !== "models-dev" && !this.#isCredentiallessProvider(provider.provider),
peekApiKey: async provider =>
preflightCompleted
? preflightApiKey
Expand Down Expand Up @@ -2244,6 +2245,8 @@ export class ModelRegistry {
case "lm-studio":
case "openai-models-list":
return this.#discoverOpenAIModelsList(providerConfig, apiKey);
case "models-dev":
return this.#discoverModelsDevProvider(providerConfig);
}
}

Expand Down Expand Up @@ -2624,22 +2627,22 @@ export class ModelRegistry {
const baseUrl = this.#normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
const modelsUrl = `${baseUrl}/models`;

const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
const requestHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
const apiKey =
discoveryApiKey ??
(this.#isCredentiallessProvider(providerConfig.provider)
? kNoAuth
: await this.authStorage.getApiKey(providerConfig.provider));
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
headers.Authorization = `Bearer ${apiKey}`;
requestHeaders.Authorization = `Bearer ${apiKey}`;
}

const [response, serverMetadata] = await Promise.all([
fetch(modelsUrl, {
headers,
headers: requestHeaders,
signal: AbortSignal.timeout(250),
}),
this.#discoverLlamaCppServerMetadata(baseUrl, headers),
this.#discoverLlamaCppServerMetadata(baseUrl, requestHeaders),
]);
if (!response.ok) {
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
Expand All @@ -2662,7 +2665,7 @@ export class ModelRegistry {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: serverMetadata?.contextWindow ?? 128000,
maxTokens: Math.min(serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY, 8192),
headers,
headers: providerConfig.headers,
compat: {
supportsStore: false,
supportsDeveloperRole: false,
Expand All @@ -2674,6 +2677,65 @@ export class ModelRegistry {
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
}

#resolveDiscoveredModelApi(providerConfig: DiscoveryProviderConfig, modelId: string): Api {
let api = providerConfig.api;
let matchedPrefixLength = -1;
for (const [prefix, routedApi] of Object.entries(providerConfig.discovery.apiByModelPrefix ?? {})) {
if (modelId.startsWith(prefix) && prefix.length > matchedPrefixLength) {
api = routedApi;
Comment on lines +2683 to +2685

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 Reapply prefix routing to cached models

When apiByModelPrefix is edited while this provider has a fresh discovery cache, the normal refresh() path uses online-if-uncached and returns the cached model objects without invoking this resolver. Those objects retain the API selected by the previous configuration, so a model can continue using Chat Completions instead of Anthropic Messages (or vice versa) for up to the 24-hour cache lifetime. Reapply the current prefix rules while normalizing cached models, or include these rules in cache invalidation.

Useful? React with 👍 / 👎.

matchedPrefixLength = prefix.length;
}
}
return api;
}

async #discoverModelsDevProvider(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
const baseUrl = providerConfig.baseUrl;

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 Rebind cached models.dev rows to the live base URL

When a models-dev provider's configured baseUrl changes while its 24-hour discovery cache is fresh, each cached model retains the old URL assigned here. #loadCachedDiscoverableModels only restores the live URL for openai-models-list and lm-studio, and online-if-uncached then accepts these rows while recording evidence for the new endpoint, so inference—and its API key—can continue going to the previous endpoint until the cache expires. Normalize cached models-dev rows against the current provider URL as well.

Useful? React with 👍 / 👎.

if (!baseUrl) throw new Error(`Provider "${providerConfig.provider}" requires baseUrl for models.dev discovery.`);
const response = await fetch("https://models.dev/api.json", {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) throw new Error(`HTTP ${response.status} from https://models.dev/api.json`);
const payload: unknown = await response.json();
if (!isRecord(payload)) return [];
const catalogProvider = payload[providerConfig.discovery.modelsDevProvider ?? providerConfig.provider];
if (!isRecord(catalogProvider) || !isRecord(catalogProvider.models)) return [];

const discovered: Model<Api>[] = [];
for (const [catalogId, value] of Object.entries(catalogProvider.models)) {
if (!isRecord(value) || value.tool_call !== true || value.status === "deprecated") 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 Remove models omitted by refreshed catalogs

When a previously discovered ClinePass model is later marked deprecated or removed, an online refresh filters it out here, but #refreshRuntimeDiscoveries merges only the returned rows into the existing this.#models array and never removes provider rows absent from the new result; its discovered.length === 0 path also returns without changing the catalog. Consequently, deprecated or withdrawn models remain selectable until the process restarts, despite a successful live refresh. Replace the provider's prior discovered slice when publishing an authoritative result.

Useful? React with 👍 / 👎.

const id = typeof value.id === "string" && value.id.trim() ? value.id : catalogId;
const limit = isRecord(value.limit) ? value.limit : {};
const cost = isRecord(value.cost) ? value.cost : {};
const modalities = isRecord(value.modalities) ? value.modalities : {};
const inputModalities = Array.isArray(modalities.input) ? modalities.input : [];
const outputModalities = Array.isArray(modalities.output) ? modalities.output : [];
discovered.push(
enrichModelThinking({
id,
name: typeof value.name === "string" && value.name.trim() ? value.name : id,
api: this.#resolveDiscoveredModelApi(providerConfig, id),
provider: providerConfig.provider,
baseUrl,
reasoning: value.reasoning === true,
input: inputModalities.includes("image") ? ["text", "image"] : ["text"],
output: outputModalities.includes("image") ? ["text", "image"] : ["text"],
cost: {
input: toPositiveNumberOrUndefined(cost.input) ?? 0,
output: toPositiveNumberOrUndefined(cost.output) ?? 0,
cacheRead: toPositiveNumberOrUndefined(cost.cache_read) ?? 0,
cacheWrite: toPositiveNumberOrUndefined(cost.cache_write) ?? 0,
},
contextWindow: toPositiveNumberOrUndefined(limit.context) ?? UNK_CONTEXT_WINDOW,
maxTokens: toPositiveNumberOrUndefined(limit.output) ?? UNK_MAX_TOKENS,
headers: providerConfig.headers,
}),
);
}
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
}

async #discoverOpenAIModelsList(
providerConfig: DiscoveryProviderConfig,
discoveryApiKey?: string,
Expand All @@ -2683,7 +2745,7 @@ export class ModelRegistry {
const requestBaseUrl = baseUrl;
modelsUrl.pathname = `${modelsUrl.pathname.replace(/\/+$/g, "")}/models`;

const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
const requestHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
// Resolve with the same baseUrl context completion requests use so an
// endpoint-scoped (or config-pinned) credential wins here exactly as it
// does for chat completions.
Expand All @@ -2693,12 +2755,12 @@ export class ModelRegistry {
? kNoAuth
: await this.authStorage.getApiKey(providerConfig.provider, undefined, { baseUrl }));
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
headers.Authorization = `Bearer ${apiKey}`;
requestHeaders.Authorization = `Bearer ${apiKey}`;
}

const response = await fetch(modelsUrl, {
headers,
signal: AbortSignal.timeout(250),
headers: requestHeaders,
signal: AbortSignal.timeout(5_000),
Comment on lines 2761 to +2763

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 Preserve the short timeout for local model discovery

When implicit LM Studio discovery is uncached and its endpoint accepts a connection but does not respond—or LM_STUDIO_BASE_URL points at a black-holed host—an awaited refresh now stalls for five seconds because lm-studio is dispatched through this same method. This path previously had a 250 ms budget and is awaited by model-selection and task-execution flows, so use the longer timeout only for remote providers such as Command Code while retaining a short timeout for optional local discovery.

Useful? React with 👍 / 👎.

});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
Expand All @@ -2710,26 +2772,33 @@ export class ModelRegistry {
}
throw new Error(`HTTP ${response.status} from ${redactDiscoveryUrl(modelsUrl)}`);
}
const payload = (await response.json()) as { data?: Array<{ id: string }> };
const payload = (await response.json()) as {
data?: Array<{ id: string; name?: string; context_length?: number }>;
};
const models = payload.data ?? [];
const discovered: Model<Api>[] = [];
for (const item of models) {
const id = item.id;
if (!id) continue;
const referenceModel = resolveCustomModelReference(id);
const api = this.#resolveDiscoveredModelApi(providerConfig, id);
discovered.push(
enrichModelThinking({
id,
name: id,
api: providerConfig.api,
name: item.name ?? referenceModel?.name ?? id,
api,
provider: providerConfig.provider,
baseUrl: requestBaseUrl,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
headers,
reasoning: referenceModel?.reasoning ?? false,
thinking: referenceModel?.thinking,
input: referenceModel?.input ?? ["text"],
output: referenceModel?.output,
cost: referenceModel?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },

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 inheriting upstream prices for proxy catalogs

When an OpenAI-compatible /models response contains an ID from the bundled catalog—such as Command Code's Claude models or a known model loaded through LM Studio—this copies the first-party model's per-token prices even though the discovered endpoint may be subscription-backed or local. calculateCost subsequently uses model.cost for these non-OpenAI providers, so session totals and exports report charges that were never incurred; keep discovery pricing at zero unless the endpoint or a provider-specific override supplies it.

Useful? React with 👍 / 👎.

contextWindow: item.context_length ?? referenceModel?.contextWindow ?? UNK_CONTEXT_WINDOW,
maxTokens: referenceModel?.maxTokens ?? UNK_MAX_TOKENS,
Comment on lines +2797 to +2798

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 Cap discovered output tokens to the advertised context

When an OpenAI-compatible /models endpoint reports a smaller context_length for a known model—such as a local proxy exposing a bundled Claude ID with a reduced context—the resulting model combines that server limit with the bundled model's potentially larger maxTokens. This can advertise an impossible output allowance and, for Anthropic-routed models, cause buildParams to derive a max_tokens value that exceeds the endpoint's total context; clamp the reference output limit to the discovered context or use endpoint-provided output metadata.

Useful? React with 👍 / 👎.

headers: providerConfig.headers,
compat: {
...referenceModel?.compat,
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
Expand Down
4 changes: 3 additions & 1 deletion packages/coding-agent/src/config/models-config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ export const ModelOverrideSchema = z
export type ModelOverride = z.infer<typeof ModelOverrideSchema>;

export const ProviderDiscoverySchema = z.object({
type: z.enum(["ollama", "llama.cpp", "lm-studio", "openai-models-list"]),
type: z.enum(["ollama", "llama.cpp", "lm-studio", "openai-models-list", "models-dev"]),
apiByModelPrefix: z.record(z.string().min(1), z.enum(["openai-completions", "anthropic-messages"])).optional(),
modelsDevProvider: z.string().min(1).optional(),
});

const LocalOpenAICompatSchema = z
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/src/config/provider-ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const FAMOUS_PROVIDER_ORDER: readonly string[] = [
"opencode-go",
"zai",
"glm-zcode",
"cline-pass",
"commandcode-goat",
"alibaba-token-plan",
"qwen-portal",
"kimi-code",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { formatProviderCredentialHint } from "@gajae-code/ai/stream";

export const MODEL_ONBOARDING_API_PROVIDER_COMMAND =
"/provider add --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <model>";
export const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND = "/provider add --preset <minimax|minimax-cn|glm>";
export const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND = "/provider add --preset <id>";

export const MODEL_ONBOARDING_SETUP_COMMAND = "gjc setup provider";
export const MODEL_ONBOARDING_OAUTH_COMMAND = "/provider login [provider-id] or /login [provider-id]";
Expand Down
Loading
Loading