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
2 changes: 2 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Fixed

- Fixed CI type-check failures from test files referencing model IDs no longer returned by upstream `/models` endpoints. `models.generated.ts` is regenerated each CI run, so tests hardcoding stale IDs (`gpt-4o` on github-copilot, `qwen-3-235b-a22b-instruct-2507` on cerebras, `google/gemini-2.0-flash-001` on openrouter) failed `getModel`'s typed lookup. Swapped to currently-served siblings (`gpt-4.1`, `llama3.1-8b`, `google/gemini-2.5-flash`).
- Fixed GitHub Copilot capability discovery silently failing on `api.individual.githubcopilot.com` (HTTP 421 Misdirected Request for some accounts): discovery now falls back through the canonical Copilot hosts (`api.githubcopilot.com`, `api.business.githubcopilot.com`, `api.enterprise.githubcopilot.com`) and uses the host that responded for the registered model's `baseUrl`, so 1M-context variants are actually surfaced.
- Fixed Copilot Anthropic model `contextWindow` being set to `max_context_window_tokens` (total input+output budget) instead of `max_prompt_tokens` (real input cap), causing the compaction threshold to never trigger for 1M-context variants until the provider rejected the request. Both the capability cache and the registered model entry now use `max_prompt_tokens`, so compaction fires at the real input ceiling.
- Fixed Anthropic adaptive-thinking rejection on `claude-opus-4-7` (and the `opus-4-6` family more generally): the request builder now consults a per-(provider, model) capability table and emits the adaptive `thinking.type=adaptive` + `output_config.effort` shape for any model that requires it, instead of the legacy `thinking.type=enabled` + `budget_tokens` shape ([#11](https://github.com/JuliusBrussee/caveman-code/issues/11))
- Fixed Anthropic 1M context tier not being opted into for `claude-opus-4-5` on the direct Anthropic API and Bedrock: the provider now sends `anthropic-beta: context-1m-2025-08-07` for that family and the model registry overrides its `contextWindow` to 1_000_000 so the modeline / picker / compaction logic see the real ceiling. The opt-in is provider-scoped: the GitHub Copilot Anthropic relay rejects this beta header and exposes 1M-context Claude models as separate model ids instead, so the beta is not sent on Copilot. Discovery of those Copilot-only model ids is out of scope for this PR; a follow-up runtime-discovery layer will surface them ([#12](https://github.com/JuliusBrussee/caveman-code/issues/12))
- Fixed bare `readline` import to use `node:readline` prefix for Deno compatibility ([#2885](https://github.com/badlogic/pi-mono/issues/2885) by [@milosv-vtool](https://github.com/milosv-vtool))
Expand Down
60 changes: 45 additions & 15 deletions packages/ai/src/providers/anthropic-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,43 @@ interface CopilotModelEntry {
};
}

// Copilot's /models endpoint is served by a different proxy than chat. Some
// accounts (individual/business/enterprise) get HTTP 421 "Misdirected
// Request" on the host that serves chat. Try the configured host first,
// then fall back to canonical hosts so discovery succeeds regardless of
// which Copilot tier the user is on.
const COPILOT_DISCOVERY_FALLBACK_HOSTS = [
"https://api.githubcopilot.com",
"https://api.individual.githubcopilot.com",
"https://api.business.githubcopilot.com",
"https://api.enterprise.githubcopilot.com",
];

async function discoverCopilot(baseUrl: string, apiKey: string, extraHeaders?: Record<string, string>): Promise<void> {
const res = await fetch(`${baseUrl}/models`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
"User-Agent": "GitHubCopilotChat/0.35.0",
"Editor-Version": "vscode/1.107.0",
"Editor-Plugin-Version": "copilot-chat/0.35.0",
"Copilot-Integration-Id": "vscode-chat",
...extraHeaders,
},
});
if (!res.ok) return;
const headers = {
Authorization: `Bearer ${apiKey}`,
"User-Agent": "GitHubCopilotChat/0.35.0",
"Editor-Version": "vscode/1.107.0",
"Editor-Plugin-Version": "copilot-chat/0.35.0",
"Copilot-Integration-Id": "vscode-chat",
...extraHeaders,
};
const hosts = [baseUrl, ...COPILOT_DISCOVERY_FALLBACK_HOSTS.filter((h) => h !== baseUrl)];
let res: Response | undefined;
let workingHost = baseUrl;
for (const host of hosts) {
try {
const r = await fetch(`${host}/models`, { method: "GET", headers });
if (r.ok) {
res = r;
workingHost = host;
break;
}
} catch {
// try next host
}
}
if (!res) return;
const body = (await res.json()) as { data?: CopilotModelEntry[] };
const entries = body?.data ?? [];

Expand All @@ -159,10 +183,16 @@ async function discoverCopilot(baseUrl: string, apiKey: string, extraHeaders?: R
const efforts = sup.reasoning_effort ?? [];
const xhighEffort = efforts.includes("max") || efforts.includes("xhigh");

// Use max_prompt_tokens (real input cap) for contextWindow, not
// max_context_window_tokens (input+output+cache total). Compaction
// compares against the input budget and the provider rejects
// requests over max_prompt_tokens.
const promptCap = lim.max_prompt_tokens ?? lim.max_context_window_tokens;

setDiscoveredCapabilities("github-copilot", entry.id, {
thinkingSchema: adaptive ? "adaptive" : "legacy",
xhighEffort: xhighEffort || undefined,
contextWindow: lim.max_context_window_tokens,
contextWindow: promptCap,
});

// Build a registry entry so previously-unknown ids (e.g.
Expand All @@ -172,7 +202,7 @@ async function discoverCopilot(baseUrl: string, apiKey: string, extraHeaders?: R
name: entry.name || entry.id,
api: "anthropic-messages",
provider: "github-copilot",
baseUrl,
baseUrl: workingHost,
headers: {
"User-Agent": "GitHubCopilotChat/0.35.0",
"Editor-Version": "vscode/1.107.0",
Expand All @@ -182,7 +212,7 @@ async function discoverCopilot(baseUrl: string, apiKey: string, extraHeaders?: R
reasoning: adaptive,
input: sup.vision ? ["text", "image"] : ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: lim.max_context_window_tokens ?? 128000,
contextWindow: promptCap ?? 128000,
maxTokens: lim.max_output_tokens ?? 8192,
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Fixed

- Fixed initial model resolution falling back to a different model when the user's saved default isn't in the static registry (e.g. discovered-only Copilot ids). `createAgentSession` and the session-services factory now await Anthropic capability discovery before resolving the initial model if the saved default is missing from the snapshot, so the correct `contextWindow` is used from the first turn instead of silently degrading to a smaller-window sibling and over-compacting.
- Added missing `@sinclair/typebox` runtime dependency. Fixes `ERR_MODULE_NOT_FOUND` on `caveman-code` startup when installed globally ([#6](https://github.com/JuliusBrussee/caveman-code/issues/6)).
- Fixed broken extensions-migration links in the hooks-folder warning — now point to `JuliusBrussee/caveman-code` instead of the renamed-away `caveman-cli` repo ([#8](https://github.com/JuliusBrussee/caveman-code/issues/8)).
- Self-update check now queries the correct GitHub repo (`caveman-code`) for releases.
Expand Down
11 changes: 9 additions & 2 deletions packages/coding-agent/src/core/agent-session-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,15 @@ export async function createAgentSessionServices(
const authStorage = options.authStorage ?? AuthStorage.create(join(agentDir, "auth.json"));
const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
const modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, join(agentDir, "models.json"));
// See sdk.ts for rationale; fire-and-forget Anthropic capability discovery.
void modelRegistry.discoverAnthropicCapabilities().catch(() => {});
// Anthropic capability discovery. If the user's saved default model isn't
// in the static registry, await discovery so resolution sees the freshly
// merged ids (e.g. github-copilot's claude-opus-4.7-1m-internal).
const discoveryPromise = modelRegistry.discoverAnthropicCapabilities().catch(() => {});
const savedDefaultProvider = settingsManager.getDefaultProvider();
const savedDefaultModelId = settingsManager.getDefaultModel();
if (savedDefaultProvider && savedDefaultModelId && !modelRegistry.find(savedDefaultProvider, savedDefaultModelId)) {
await discoveryPromise;
}
const resourceLoader = new DefaultResourceLoader({
...(options.resourceLoaderOptions ?? {}),
cwd,
Expand Down
16 changes: 12 additions & 4 deletions packages/coding-agent/src/core/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const authStorage = options.authStorage ?? AuthStorage.create(authPath);
const modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);

const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);

// Kick off per-account capability discovery for Anthropic-API providers
// (e.g. GitHub Copilot's /models endpoint surfaces 1M variants like
// claude-opus-4.6-1m that aren't in the static registry). Fire-and-forget;
// onModelRegistryChange refreshes the local snapshot as results arrive.
void modelRegistry.discoverAnthropicCapabilities().catch(() => {});
// claude-opus-4.6-1m that aren't in the static registry).
// If the user's saved default model isn't in the static registry, we MUST
// await discovery before resolving — otherwise resolution falls back to a
// different model with a smaller context window. Otherwise fire-and-forget.
const discoveryPromise = modelRegistry.discoverAnthropicCapabilities().catch(() => {});
const savedDefaultProvider = settingsManager.getDefaultProvider();
const savedDefaultModelId = settingsManager.getDefaultModel();
if (savedDefaultProvider && savedDefaultModelId && !modelRegistry.find(savedDefaultProvider, savedDefaultModelId)) {
await discoveryPromise;
}

const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
const sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));

if (!resourceLoader) {
Expand Down
Loading