Skip to content

Commit d3f60ba

Browse files
sestinjcursoragent
andauthored
fix(cli): use explicit model definitions in default config instead of Hub slugs (#12804)
* fix(cli): use explicit model definitions in default config instead of Hub slugs The CLI onboarding wrote a default ~/.continue/config.yaml using slug-based blocks (`uses: anthropic/claude-sonnet-4-6` with `with: { ANTHROPIC_API_KEY }`). Slug/Hub resolution has been removed, so the CLI errored at runtime with "Failed to unroll block ... Slug-based package resolution is not supported". Replace the slug block with explicit Anthropic model definitions (Claude Sonnet 4.6 + Claude Opus 4.6), mirroring core/config/onboarding.ts. Legacy slug-based anthropic blocks in an existing config are now migrated to explicit defs. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): update headless e2e for explicit default model With the default config now creating an explicit Anthropic model (instead of an unresolvable slug), the headless run reaches the provider and fails on the invalid API key rather than failing earlier with "no model specified". Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 18acf6f commit d3f60ba

3 files changed

Lines changed: 145 additions & 69 deletions

File tree

‎extensions/cli/src/e2e/headless-anthropic-api-key.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,12 @@ models:
9494
timeout: 15000,
9595
});
9696

97-
// The CLI should fail because auto-config from ANTHROPIC_API_KEY no longer creates a model
97+
// The CLI auto-creates an explicit Anthropic model from ANTHROPIC_API_KEY,
98+
// so it now reaches the provider and fails on the invalid key rather than
99+
// failing earlier with "no model specified".
98100
expect(result.exitCode).toBe(1);
99101

100-
// Should contain error about no model being specified
101-
expect(result.stderr).toContain("No model specified in headless mode");
102+
// Should contain an authentication error from the invalid API key
103+
expect(result.stderr).toContain("invalid x-api-key");
102104
}, 20000);
103105
});

‎extensions/cli/src/util/yamlConfigUpdater.test.ts‎

Lines changed: 76 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@ describe("updateAnthropicModelInYaml", () => {
1212
expect(result).toContain("name: Main Config");
1313
expect(result).toContain("version: 1.0.0");
1414
expect(result).toContain("schema: v1");
15-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
16-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
15+
expect(result).toContain("provider: anthropic");
16+
expect(result).toContain("model: claude-sonnet-4-6");
17+
expect(result).toContain("apiKey: sk-ant-test123456789");
18+
expect(result).not.toContain("uses:");
1719
});
1820

1921
it("should create new config from invalid YAML", () => {
2022
const invalidYaml = "invalid: [yaml content";
2123
const result = updateAnthropicModelInYaml(invalidYaml, testApiKey);
2224

2325
expect(result).toContain("name: Main Config");
24-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
25-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
26+
expect(result).toContain("model: claude-sonnet-4-6");
27+
expect(result).toContain("apiKey: sk-ant-test123456789");
28+
expect(result).not.toContain("uses:");
2629
});
2730
});
2831

@@ -34,18 +37,19 @@ version: 1.0.0
3437
schema: v1
3538
# List of available models
3639
models:
37-
- uses: openai/gpt-4
38-
with:
39-
OPENAI_API_KEY: TEST-openai-test
40+
- name: GPT-4
41+
provider: openai
42+
model: gpt-4
43+
apiKey: TEST-openai-test
4044
`;
4145

4246
const result = updateAnthropicModelInYaml(yamlWithComments, testApiKey);
4347

4448
expect(result).toContain("# My Continue config");
4549
expect(result).toContain("# List of available models");
46-
expect(result).toContain("uses: openai/gpt-4");
47-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
48-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
50+
expect(result).toContain("model: gpt-4");
51+
expect(result).toContain("model: claude-sonnet-4-6");
52+
expect(result).toContain("apiKey: sk-ant-test123456789");
4953
});
5054

5155
it("should preserve comments when updating existing model", () => {
@@ -55,19 +59,39 @@ version: 1.0.0
5559
schema: v1
5660
# List of available models
5761
models:
58-
- uses: anthropic/claude-sonnet-4-6
59-
with:
60-
ANTHROPIC_API_KEY: old-key
62+
- name: Claude Sonnet 4.6
63+
provider: anthropic
64+
model: claude-sonnet-4-6
65+
apiKey: old-key
6166
`;
6267

6368
const result = updateAnthropicModelInYaml(yamlWithComments, testApiKey);
6469

6570
expect(result).toContain("# My Continue config");
6671
expect(result).toContain("# List of available models");
67-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
68-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
72+
expect(result).toContain("model: claude-sonnet-4-6");
73+
expect(result).toContain("apiKey: sk-ant-test123456789");
6974
expect(result).not.toContain("old-key");
7075
});
76+
77+
it("should replace legacy slug-based anthropic blocks", () => {
78+
const yamlWithSlug = `name: Main Config
79+
version: 1.0.0
80+
schema: v1
81+
models:
82+
- uses: anthropic/claude-sonnet-4-6
83+
with:
84+
ANTHROPIC_API_KEY: old-key
85+
`;
86+
87+
const result = updateAnthropicModelInYaml(yamlWithSlug, testApiKey);
88+
89+
expect(result).not.toContain("uses:");
90+
expect(result).not.toContain("old-key");
91+
expect(result).toContain("provider: anthropic");
92+
expect(result).toContain("model: claude-sonnet-4-6");
93+
expect(result).toContain("apiKey: sk-ant-test123456789");
94+
});
7195
});
7296

7397
describe("model management", () => {
@@ -76,45 +100,46 @@ models:
76100
version: 1.0.0
77101
schema: v1
78102
models:
79-
- uses: openai/gpt-4
80-
with:
81-
OPENAI_API_KEY: TEST-openai-test
103+
- name: GPT-4
104+
provider: openai
105+
model: gpt-4
106+
apiKey: TEST-openai-test
82107
`;
83108

84109
const result = updateAnthropicModelInYaml(existingConfig, testApiKey);
85110

86-
expect(result).toContain("uses: openai/gpt-4");
87-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
88-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
89-
expect(result).toContain("OPENAI_API_KEY: TEST-openai-test");
111+
expect(result).toContain("model: gpt-4");
112+
expect(result).toContain("model: claude-sonnet-4-6");
113+
expect(result).toContain("apiKey: sk-ant-test123456789");
114+
expect(result).toContain("apiKey: TEST-openai-test");
90115
});
91116

92117
it("should update existing anthropic model", () => {
93118
const existingConfig = `name: Main Config
94119
version: 1.0.0
95120
schema: v1
96121
models:
97-
- uses: anthropic/claude-sonnet-4-6
98-
with:
99-
ANTHROPIC_API_KEY: old-anthropic-key
100-
- uses: openai/gpt-4
101-
with:
102-
OPENAI_API_KEY: TEST-openai-test
122+
- name: Claude Sonnet 4.6
123+
provider: anthropic
124+
model: claude-sonnet-4-6
125+
apiKey: old-anthropic-key
126+
- name: GPT-4
127+
provider: openai
128+
model: gpt-4
129+
apiKey: TEST-openai-test
103130
`;
104131

105132
const result = updateAnthropicModelInYaml(existingConfig, testApiKey);
106133

107-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
108-
expect(result).toContain("uses: openai/gpt-4");
109-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
110-
expect(result).toContain("OPENAI_API_KEY: TEST-openai-test");
134+
expect(result).toContain("model: claude-sonnet-4-6");
135+
expect(result).toContain("model: gpt-4");
136+
expect(result).toContain("apiKey: sk-ant-test123456789");
137+
expect(result).toContain("apiKey: TEST-openai-test");
111138
expect(result).not.toContain("old-anthropic-key");
112139

113-
// Should only have one anthropic model
114-
const anthropicMatches = result.match(
115-
/uses: anthropic\/claude-sonnet-4-6/g,
116-
);
117-
expect(anthropicMatches).toHaveLength(1);
140+
// Should only have one Claude Sonnet model
141+
const sonnetMatches = result.match(/model: claude-sonnet-4-6/g);
142+
expect(sonnetMatches).toHaveLength(1);
118143
});
119144

120145
it("should handle config with no models array", () => {
@@ -130,8 +155,8 @@ schema: v1
130155

131156
expect(result).toContain("name: Main Config");
132157
expect(result).toContain("models:");
133-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
134-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
158+
expect(result).toContain("model: claude-sonnet-4-6");
159+
expect(result).toContain("apiKey: sk-ant-test123456789");
135160
});
136161

137162
it("should handle config with empty models array", () => {
@@ -147,8 +172,8 @@ models: []
147172
);
148173

149174
expect(result).toContain("name: Main Config");
150-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
151-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
175+
expect(result).toContain("model: claude-sonnet-4-6");
176+
expect(result).toContain("apiKey: sk-ant-test123456789");
152177
});
153178
});
154179

@@ -157,7 +182,10 @@ models: []
157182
const input = `# Test config
158183
name: Test
159184
models:
160-
- uses: existing/model
185+
- name: Existing
186+
provider: openai
187+
model: existing-model
188+
apiKey: test
161189
`;
162190

163191
const result = updateAnthropicModelInYaml(input, testApiKey);
@@ -175,9 +203,9 @@ models:
175203
expect(result).toMatch(/^version: /m);
176204
expect(result).toMatch(/^schema: /m);
177205
expect(result).toMatch(/^models:/m);
178-
expect(result).toMatch(/^\s+- uses: /m);
179-
expect(result).toMatch(/^\s+with:/m);
180-
expect(result).toMatch(/^\s+ANTHROPIC_API_KEY: /m);
206+
expect(result).toMatch(/^\s+- name: /m);
207+
expect(result).toMatch(/^\s+provider: anthropic/m);
208+
expect(result).toMatch(/^\s+apiKey: /m);
181209
});
182210
});
183211

@@ -189,8 +217,8 @@ models: "not an array"
189217

190218
const result = updateAnthropicModelInYaml(malformedConfig, testApiKey);
191219

192-
expect(result).toContain("uses: anthropic/claude-sonnet-4-6");
193-
expect(result).toContain("ANTHROPIC_API_KEY: sk-ant-test123456789");
220+
expect(result).toContain("model: claude-sonnet-4-6");
221+
expect(result).toContain("apiKey: sk-ant-test123456789");
194222
});
195223

196224
it("should handle different API key formats", () => {
@@ -202,7 +230,7 @@ models: "not an array"
202230

203231
differentKeys.forEach((key) => {
204232
const result = updateAnthropicModelInYaml("", key);
205-
expect(result).toContain(`ANTHROPIC_API_KEY: ${key}`);
233+
expect(result).toContain(`apiKey: ${key}`);
206234
});
207235
});
208236
});

‎extensions/cli/src/util/yamlConfigUpdater.ts‎

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { parseDocument } from "yaml";
22

33
export interface ModelConfig {
4-
uses: string;
5-
with: {
6-
ANTHROPIC_API_KEY: string;
4+
name: string;
5+
provider: string;
6+
model: string;
7+
apiKey: string;
8+
roles: string[];
9+
defaultCompletionOptions?: {
10+
contextLength: number;
11+
maxTokens: number;
712
};
13+
capabilities?: string[];
814
}
915

1016
export interface ConfigStructure {
@@ -14,9 +20,54 @@ export interface ConfigStructure {
1420
models: ModelConfig[];
1521
}
1622

23+
// These model definitions are inlined copies of the corresponding Continue Hub
24+
// blocks (e.g. anthropic/claude-sonnet-4-6) that onboarding previously resolved
25+
// via `uses:` slugs. Since Hub/slug resolution has been removed, we reproduce
26+
// the exact block contents here, with `apiKey` substituted for the block's
27+
// `${{ inputs.*_API_KEY }}` placeholder. Keep these in sync with the explicit
28+
// Anthropic models in core/config/onboarding.ts.
29+
function getAnthropicModels(apiKey: string): ModelConfig[] {
30+
return [
31+
{
32+
name: "Claude Sonnet 4.6",
33+
provider: "anthropic",
34+
model: "claude-sonnet-4-6",
35+
apiKey,
36+
roles: ["chat", "edit", "apply"],
37+
defaultCompletionOptions: { contextLength: 200000, maxTokens: 64000 },
38+
capabilities: ["tool_use", "image_input"],
39+
},
40+
{
41+
name: "Claude Opus 4.6",
42+
provider: "anthropic",
43+
model: "claude-opus-4-6",
44+
apiKey,
45+
roles: ["chat", "edit", "apply"],
46+
defaultCompletionOptions: { contextLength: 200000, maxTokens: 64000 },
47+
capabilities: ["tool_use", "image_input"],
48+
},
49+
];
50+
}
51+
52+
function isManagedAnthropicModel(model: any): boolean {
53+
if (!model || typeof model !== "object") {
54+
return false;
55+
}
56+
// Drop legacy slug-based blocks (e.g. `uses: anthropic/claude-sonnet-4-6`)...
57+
if (typeof model.uses === "string" && model.uses.startsWith("anthropic/")) {
58+
return true;
59+
}
60+
// ...as well as the explicit Anthropic models we manage here.
61+
return (
62+
model.provider === "anthropic" &&
63+
(model.model === "claude-sonnet-4-6" || model.model === "claude-opus-4-6")
64+
);
65+
}
66+
1767
/**
18-
* Updates or adds an Anthropic Claude model configuration in a YAML string while preserving comments and formatting.
19-
* This is a pure function that takes a YAML string and returns a modified YAML string.
68+
* Updates or adds explicit Anthropic Claude model configurations in a YAML
69+
* string while preserving comments and formatting. This is a pure function that
70+
* takes a YAML string and returns a modified YAML string.
2071
*
2172
* @param yamlContent - The original YAML content as a string (can be empty)
2273
* @param apiKey - The Anthropic API key to set
@@ -26,12 +77,7 @@ export function updateAnthropicModelInYaml(
2677
yamlContent: string,
2778
apiKey: string,
2879
): string {
29-
const newModel: ModelConfig = {
30-
uses: "anthropic/claude-sonnet-4-6",
31-
with: {
32-
ANTHROPIC_API_KEY: apiKey,
33-
},
34-
};
80+
const newModels = getAnthropicModels(apiKey);
3581

3682
try {
3783
const doc = parseDocument(yamlContent);
@@ -42,7 +88,7 @@ export function updateAnthropicModelInYaml(
4288
name: "Main Config",
4389
version: "1.0.0",
4490
schema: "v1",
45-
models: [newModel],
91+
models: newModels,
4692
};
4793

4894
const newDoc = parseDocument("");
@@ -56,17 +102,17 @@ export function updateAnthropicModelInYaml(
56102
const config = doc.toJS() as any;
57103

58104
// Make sure models array exists
59-
if (!config.models) {
105+
if (!config.models || !Array.isArray(config.models)) {
60106
config.models = [];
61107
}
62108

63-
// Filter out existing anthropic models
109+
// Filter out existing Anthropic models (legacy slug blocks + managed models)
64110
config.models = config.models.filter(
65-
(model: any) => !model || model.uses !== "anthropic/claude-sonnet-4-6",
111+
(model: any) => !isManagedAnthropicModel(model),
66112
);
67113

68-
// Add the new anthropic model
69-
config.models.push(newModel);
114+
// Add the new explicit Anthropic models
115+
config.models.push(...newModels);
70116

71117
// Update the models array while preserving comments and structure
72118
doc.set("models", config.models);
@@ -78,7 +124,7 @@ export function updateAnthropicModelInYaml(
78124
name: "Main Config",
79125
version: "1.0.0",
80126
schema: "v1",
81-
models: [newModel],
127+
models: newModels,
82128
};
83129

84130
const doc = parseDocument("");

0 commit comments

Comments
 (0)