18 Commits
Author SHA1 Message Date
bb6d66faa3 feat(providers): live model lists for OpenRouter and OpenGateway (#2084)
* feat(providers): fetch live model lists for OpenRouter and OpenGateway

Enable hybrid discovery so OpenGateway and OpenRouter load public
GET /v1/models catalogs (with coding filters on OpenRouter), matching
cairn-code and the Zero live-list fix.

Refs #2083

* Address live model discovery review feedback.

Remove hardcoded model allowlisting, deduplicate live MiMo routes, avoid duplicate startup probes, share mapping helpers, and strengthen provider documentation and tests.\n\nRefs #2083

* test(providers): Isolate OpenGateway picker discovery state.

Prevent persisted live discovery cache entries from making the static catalog assertion nondeterministic.

Refs #2083

* fix(test): restore OPENGATEWAY_API_KEY after discovery test

The no-auth OpenGateway discovery test deletes OPENGATEWAY_API_KEY but
originalEnv never snapshotted it and afterEach never restored it, so a
worker starting with the credential set would run every later test in
that worker without it. Snapshot and restore it like the other provider
env vars.

Refs #2084

* fix(integrations): preserve route shim maxTokensField for live-only discovered models

* fix(test): drop unrelated permissions.test.ts optional-chaining tweak

Not part of the OpenGateway/OpenRouter live discovery change; jatmn's
review on #2084 flagged it as unrelated drift that should be dropped
or split into its own PR.

Refs #2084

* docs(integrations): Add JSDoc comments to model mapping helpers

Add detailed JSDoc documentation for gateway model normalization,
tooling and reasoning support detection, and core model mapping type
guards and helpers across OpenGateway, OpenRouter, and modelMapping.

Refs #2083

* fix(integrations): address review feedback on live discovery and proxy credentials

Preserve caller credentials and custom headers for private route overrides, remove deep-research exclusion for text models, isolate test config directories, and align model picker assertions with upstream curated models.

Refs #2083
Refs #2084

* test(integrations): test explicit openaiShim precedence and removeBodyFields merge

Add unit test assertions verifying that explicit descriptor and catalog openaiShim configurations take precedence over inferred model settings and that removeBodyFields arrays merge correctly across layers.

Refs #2083
Refs #2084

* Filter expired catalog entries at discovery boundary and revert permissions test hunk.

Wrap static and merged route catalog model lists in filterAvailableCatalogEntries across all discoverModelsForRoute and refreshStartupDiscoveryForRoute return paths, preventing expired time-boxed catalog entries and live duplicates from resurfacing in model picker refresh, summary, or bootstrap additional options. Also restore permissions.test.ts to upstream/main without optional-chaining.

Refs #2084

* Fix ModelCatalogEntry type import in model test suite.

Import ModelCatalogEntry from descriptors.js rather than index.js to satisfy typecheck.

Refs #2084

---------

Co-authored-by: euxaristia <euxaristia@users.noreply.github.com>
2026-08-24 10:17:06 +08:00
a5b277971d feat(settings): add settings-based subscription override and agy terminal support (#1731)
* feat(settings): add settings-based subscription override and agy terminal support

* feat(provider): rebrand Gemini wizard setup steps to Google AI / Gemini

* fix(settings): restrict subscription override to trusted sources

Addresses jatmn's CodeRabbit review on #1731.

P1 — untrusted settings sources could spoof subscriber state:
  Reads subscriptionType only from policy/flag/user/local settings,
  excluding project and repository settings which can be checked into
  shared repos. Adds getTrustedSubscriptionType() helper used by both
  isClaudeAISubscriber() and getSubscriptionType().

P2 — subscriptionType: "free" did not short-circuit OAuth-detected
  subscriber state. With a valid Claude AI OAuth token and
  subscriptionType: "free" in settings, isClaudeAISubscriber() still
  returned true. Now "free" is authoritative and returns false.

P3 — test isolation: added afterEach(mock.restore()) to auth.test.ts
  so Bun's module mocks cannot leak into subsequent tests.

Also splits the antigravity askpass test into focused cases for
"agy" and "antigravity" substrings.

* fix(settings): restrict subscriptionType override to user settings and return false early for free

* fix(auth): clean up unused import and return type matching in tests

* fix(provider): align Gemini chooser copy with the wizard's actual auth methods

Addresses jatmn's review on #1731. The /provider chooser advertised "Google AI /
Gemini Subscription" and "Use your Google AI Premium plan, …", but the Gemini
setup wizard only offers three auth methods — API key, access token, and local
ADC — with no subscription/Premium sign-in flow. Drop the "Subscription" label
and "Google AI Premium plan" framing so the chooser matches the wizard (and the
"Google AI / Gemini" wording used elsewhere in the file). No OAuth/subscription
sign-in is planned: using Google AI/Gemini subscriptions via third-party tools
violates Google's Terms of Service.

* fix(pr1731): resolve review follow-ups

* fix(pr1731): close review follow-ups

* fix(pr1731): close antigravity and gemini follow-ups

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-07 22:02:36 +08:00
b8c8b2417b fix(opencode-go): surface clear error on subscription quota exhaustion (#1749)
* fix(opencode-go): surface clear error on subscription quota exhaustion

The opencode.ai/zen/go gateway returns 429 with FreeUsageLimitError or
GoUsageLimitError in the body when a user's Go subscription quota runs
out. Previously these fell through to the generic "Request rejected
(429)" path, causing a mysterious stop with no actionable hint.

Detect the opencode-go-specific error markers, surface a clear message
with the upgrade URL (free tier) or reset duration + workspace + limit
name (paid tier), and skip retry — the quota is terminal until reset.

Mirrors the canonical implementation in anomalyco/opencode
packages/opencode/src/session/retry.ts.

* fix(api): abort retry and avoid thinking hang on quota/allotment exhaustion

* fix: implement request URL precedence and add test coverage

* fix(api): make 'x-opencode-request-url' header authoritative for OpenCode Go errors

* fix(api): preserve OpenCode Go quota message and reuse non-stream converter in JSON fallback

Addresses the two remaining P2 review items on #1749.

withRetry: the early isQuotaExhausted guard wrapped OpenCode Go
FreeUsageLimitError/GoUsageLimitError 429s in the generic "API quota
exhausted or not enabled" message, clobbering the actionable subscribe/
reset guidance. Skip the generic guard for OpenCode Go quota errors so they
fall through to the standard shouldRetry=false terminal path, which rethrows
the original APIError and lets getAssistantMessageFromError surface the
specific message. Consolidate detection in a shared isOpenCodeGoQuotaError
predicate (errors.ts) and drop the duplicated inline header check in
shouldRetry.

openaiShim: the application/json fallback in openaiStreamToAnthropic
hand-rolled a thin converter that dropped tool_calls, forwarded raw OpenAI
finish_reason values as Anthropic stop reasons, skipped array-content
normalization, bypassed <think> stripping, and lost raw text tool-call
recovery. Extract the established non-streaming conversion into a shared
convertNonStreamingResponseToAnthropicMessage and route the fallback through
it, re-emitting the result as stream events. _convertNonStreamingResponse
now delegates to the same function.

Adds regression coverage: a withRetry test proving the OpenCode Go message
survives the retry loop, and JSON-fallback tests for tool_calls, stop-reason
mapping, <think> stripping, array content, and raw text tool-call recovery.

* fix(api): terminate OpenCode Go quota in fast mode; treat empty tool_calls as absent

Addresses CodeRabbit's review on #1749.

- [Major] withRetry.ts: throw CannotRetryError for isOpenCodeGoQuotaError BEFORE
  the fast-mode 429 fallback. Previously the guard only *skipped* the generic
  quota throw, so an OpenCode Go 429 while fast mode was active hit the fast-mode
  retry/cooldown path instead of surfacing the quota message immediately.
  Wrapping the original APIError still preserves the OpenCode Go assistant
  message via getAssistantMessageFromError. Adds a fast-mode regression test
  (mutation-checked) and a forceFastMode option on the test helper.

- [Minor] openaiShim.ts: an empty tool_calls array is truthy, which skipped the
  raw "Tool calls requested" recovery in convertNonStreamingResponseToAnthropicMessage.
  Gate on a single hasStructuredToolCalls (length > 0) check across both the
  string- and array-content raw-recovery paths and the structured loop. Adds a
  JSON-fallback regression test for tool_calls: [] (mutation-checked).

- [Minor] openaiShim.test.ts: collectFallbackEvents now saves and restores
  globalThis.fetch in a finally block so the stub can't leak past the helper.

* test(openai-shim): cover empty tool_calls raw-recovery on array content too

Addresses CodeRabbit's follow-up on #1749: the empty-tool_calls regression only
exercised the string-content path, but the hasStructuredToolCalls fix also gates
the array-content branch. Add a companion JSON-fallback test with array-form
message.content and tool_calls: [] so the array branch can't regress silently.
Mutation-checked: reverting hasStructuredToolCalls to a truthiness check fails it.

* fix(api): preserve OpenCode Go quota errors

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-07 13:20:11 +08:00
euxaristiaandGitHub 4be017bd4d fix: resolve zai-compatible config for all GLM remote models (#1752)
* fix: resolve zai-compatible config for all GLM remote models

* fix: address PR reviews for GLM thinking continuation, custom aliases, Fireworks catalog entries, and reasoning effort preservation

* fix(integrations): gate GLM Z.AI shim to non-catalog routes

The name-based matcher applied the full Z.AI reasoning contract to any
model path containing `glm-<digit>`, overriding catalog-backed non-Z.AI
routes (NEAR AI `zai-org/GLM-5.1-FP8`, Fireworks `glm-5p2`). Now the GLM
branch fires only when there is no catalog entry; Z.AI-contract GLM
routes (opencode-go, atlas-cloud) declare the shim explicitly via
transportOverrides, and a shared ZAI_GLM_OPENAI_SHIM constant is the
single source of truth. Adds negative (NEAR AI) and positive
(opencode-go, atlas) regressions.

* test(integrations): cover the direct Z.AI vendor catalog route in the gating block

Addresses CodeRabbit's nitpick on #1752: the GLM catalog-aware gating describe
block had NEAR AI (negative), opencode-go, and atlas-cloud (positive) cases but
no direct `zai` vendor catalog positive. Add one asserting the full GLM contract
(routeId 'zai', preserveReasoningContent, thinkingRequestFormat 'zai-compatible',
requireReasoningContentOnAssistantMessages) alongside the override-based routes.

* fix(effort): extend supportsZaiReasoningEffort for provider-prefixed GLM-5.2 models

The previous implementation only matched bare 'glm-5.2' and 'zai-org/glm-5.2'. When accessed via an aggregator alias like 'openrouter/zhipu/glm-5.2', the model name doesn't start with 'glm-' or match the zai-org prefix, so supportsZaiReasoningEffort returned false and reasoning_effort was omitted from the request body. Add an endsWith('/glm-5.2') fallback to match any provider-scoped path ending in the base model name.

* fix(gateways): wire Z.AI GLM shim to atlas-cloud GLM entries and opengateway GLM 5.2

- Add enableToolStreaming to ZAI_GLM_OPENAI_SHIM shared constant
- Apply transportOverrides.openaiShim to all 7 atlas-cloud zai-org/glm-*
  entries so catalog-backed routes get the full Z.AI shim contract
- Layer Z.AI-specific overrides (thinkingRequestFormat,
  preserveReasoningContent, enableToolStreaming) on the opengateway-glm-5.2
  entry without conflicting with the gateway-level max_completion_tokens
  and removeBodyFields

Refs #1752

* refactor(gateways): derive opengateway GLM shim from shared ZAI_GLM_OPENAI_SHIM

* fix(gateways): align Atlas + OpenCode Zen GLM entries with Z.AI wire format

- Atlas Cloud GLM entries: wireFormat 'reasoning_effort' → 'zai_compatible'

- OpenCode Go catalogEntry: add reasoning + capabilities for zaiGlm models

- OpenCode Go model descriptors: add reasoning: true for GLM entries

* fix(integrations): rebase onto upstream/main, fix Atlas/OpenCode Zen GLM wiring per review

Rebased onto upstream/main (13cf30af) which brought in verified Moonshot/Kimi Code effort metadata.

Changes:

- atlas-cloud.ts: restored upstream moonshot/grok entries; kept GLM entries with wireFormat: 'zai_compatible' + ZAI_GLM_OPENAI_SHIM

- opencode.ts: added zaiGlm metadata + ZAI_GLM_OPENAI_SHIM for glm-5.1 and glm-5

- effort.codex.test.ts: updated expectations for glm-5.2 (zai_compatible), split GLM into separate verification loop

- runtimeMetadata.test.ts: merged upstream Moonshot/Kimi Code tests, updated Atlas Cloud maxOutputTokens

* fix(integrations): type opencode zen openaiShim and test zen GLM catalog-aware overrides

* fix(integrations): restrict Z.AI GLM effort levels and add Atlas GLM regression test
2026-07-07 11:55:47 +08:00
euxaristiaandGitHub 885bd81045 fix(hicap): add missing hicap-claude-opus-4.7 catalog entry (#1797)
* fix(hicap): add missing hicap-claude-opus-4.7 catalog entry

* test(integrations): cover hicap-claude-opus-4.7 runtime limits and catalog entry

* fix(hicap): add claude-opus-4-7 alias and update provider request test

* test(hicap): cover opus 4.7 catalog aliases
2026-07-07 11:55:02 +08:00
euxaristiaandGitHub 78a4b99f68 [codex] Enable Windows long paths for worktrees (#1729)
* fix(worktree): enable Windows long paths

* fix(worktree): gate core.longpaths behind worktree.enableGitLongPaths setting + add regression tests

* fix(worktree): rename enableGitLongPaths to autoConfigureLongPaths, prevent global mock module leakage in tests

* fix(worktree): isolate platform and mock dependencies in tests to prevent global mock leak

* fix(worktree): map legacy worktree settings, route sparse-checkout errors through formatter, and add strict types
2026-07-07 11:54:38 +08:00
euxaristiaandGitHub 4a60c0f30f fix: guard convertToLogOption against empty transcript (#1723)
* fix: guard convertToLogOption against empty transcript

* test: add regression test for convertToLogOption empty transcript guard
2026-07-07 10:50:34 +08:00
e9a3c308fc feat(skills): add PDF generation skill with native TypeScript implementation (#1718)
* feat(skills): add PDF generation skill with native TypeScript implementation

- Adds /pdf bundled skill for creating PDF documents from structured content
- Pure TypeScript PDF generator (~350 lines) with zero external dependencies
- Supports headings, paragraphs, bullet/numbered lists, code blocks, tables, images, HRs, spacers
- PDF spec 1.4 compliant with WinAnsiEncoding for common special characters
- Text wrapping, font sizing, and page layout handled automatically
- Uses embedded files pattern: pdfgen.ts extracted to CLAUDE_SKILL_DIR at runtime
- Model writes a TS script using the library, executes via bun run
- No binary dependencies, no pdfgen Rust tool, no system packages needed

* fix(pdf): address review feedback — escape template interpolations, fix object numbering, remove stub merge/split

* fix(pdf): address remaining review feedback — CLAUDE_SKILL_DIR substitution, image support removal, auto-pagination

- Remove all ${CLAUDE_SKILL_DIR} references from prompt and getPromptForCommand;
  bundled skills prepend 'Base directory for this skill: <dir>' instead of
  substituting this variable. Import examples now use relative './pdfgen'.

- Remove unimplemented image support:
  - Remove { type: 'image' } from PDFElement in both prompt and pdfgen.ts
  - Remove ImageData interface and all image XObject handling in PDFWriter
  - Remove basename import (only used by image case)
  - Remove 'Use relative paths for images' rule from prompt

- Add automatic multi-page continuation for overflowing content:
  - Rename buildPageStream → buildPageStreams (returns PageStreamResult[])
  - When y < maxY, flush current page and continue on a new page
  - Code blocks that don't fit start on a new page
  - PDFWriter.build creates a separate page object per content stream
  - Replace el.rows.indexOf(row) with index variable for O(n) table rendering

Addresses review feedback from jatmn (round 2): R2-P2 x3

* fix(pdf): address CodeRabbit review — fonts, header/footer, CLI safety

- Create 8 distinct font objects (Helvetica/Bold/Oblique/BoldOblique +
  Courier/Bold/Oblique/BoldOblique) instead of one shared Helvetica.
  F1..F8 now reference separate objects (3..10) so bold/italic/courier
  variants actually render correctly.

- Remove unused header/footer fields from PDFPage interface in both the
  prompt and pdfgen.ts. These were silently dropped since buildPageStreams
  never received them.

- Fix CLI --spec mode: outFile is now the last non-flag argument excluding
  the spec file path, preventing data loss from overwriting the input JSON.
  Previously args.find() could pick spec.json as outFile.

* fix(pdf): address R3 review — absolute import path, table cell wrapping

- P2: Replace relative './pdfgen' import with '<skill-base-dir>/pdfgen'
  placeholder in prompt example and task instructions, instructing the
  model to save and run scripts from the extracted skill directory
- P2: Replace silent .substring(0, 50) truncation with proper text
  wrapping via wrapText() for table cells, with dynamic row heights
  based on the tallest cell in each row

* fix(pdf): anchor multi-line table cells from row top to prevent downward overflow

- Compute cellStartY from row top (y + rowH - 4) instead of row bottom
  so wrapped text flows downward within the cell boundary

* fix(pdf): split table rows that are taller than one page

R5-P2: Jatmn review — rows with very long wrapped cells could overflow
past the page bottom because pagination only checked once per row.

- Pre-compute wrapped lines for all cells in a row up front
- Render row in page-sized chunks, tracking linesRendered offset
- When a chunk fills the page, flushPage() and continue remaining
  lines on the next page, drawing per-chunk backgrounds and borders
- Cells with fewer lines than the tallest cell simply have no text
  drawn for the excess lines (no blank-line artefacts)

Fixes: Jatmn R5 finding (review 4450066815)

* fix(pdf): wrap overlong tokens and preserve WinAnsi characters

R6-P2 findings from Jatmn review (4451901963):

1. wrapText() now hard-splits tokens exceeding charsPerLine into
   chunks, preventing long URLs/IDs/hashes from rendering off-page
   or off-cell boundary as invisible text.

2. escapePdf() no longer calls toWinAnsi() again. The caller already
   passes WinAnsi-encoded text; the double-pass was dropping mapped
   characters (e.g. em-dashes, bullets) because the second pass treated
   WinAnsi byte values as unsupported Unicode and silently dropped them.

* fix(pdf): encode table headers through WinAnsi and wrap long code lines

R7-P2 fixes:

- Table headers now pass through toWinAnsi() before escapePdf(), matching
  the body text path. Headers containing em dashes, bullets, euro signs, and
  other WinAnsi characters now render correctly instead of emitting raw
  Unicode codepoints into the content stream.

- Code block lines are now wrapped to the available page width using the
  existing wrapText() helper with Courier metrics. Long URLs, hashes,
  minified lines, and other overlong tokens no longer extend past the page
  boundary.

* fix(pdf): write PDF streams as latin1 bytes

* fix(pdf): address remaining review feedback - empty page, table validation, header overflow

* fix: resolve PDFElement type, support A3 page size, and enable Windows longpaths for worktrees

* fix: address CodeRabbit feedback on PDF skill allowedTools, worktree error propagation, and unit tests

* fix: restrict allowedTools for pdf skill and restore worktree files to main

* fix(skills): normalize base-dir to forward slashes for Windows import safety

Bundled skills receive a prompt prefix "Base directory for this skill:
<baseDir>". On Windows, <baseDir> is a backslash path like
C:\Users\...\pdf, and the skill prompt instructs the model to do
import { createPDF } from '<skill-base-dir>/pdfgen'

Embedding a backslash path into a single-quoted JS string breaks Bun
resolution because backslashes are treated as escape characters.

Normalize baseDir to forward slashes in prependBaseDir() before
building the prefix. Forward-slash paths work cross-platform in
TypeScript/Bun import statements, so the model can safely interpolate
the path verbatim.

Addresses jatmn's review on #1718 (Windows import path).

---------

Co-authored-by: SuperDuperZed <superduperzed@users.noreply.github.com>
2026-07-07 10:48:21 +08:00
euxaristiaandGitHub 9c298112dc feat(claude): add Opus 4.8 model support (#1769)
* feat(claude): add Opus 4.8 model support

Adds Claude Opus 4.8 alongside 4.7 in the model registry, picker,
pricing, integrations, and 1M-context support. Mirrors the established
4.7 pattern so longer suffixes resolve first in canonical-name matching.

- configs: CLAUDE_OPUS_4_8_CONFIG + opus48 registry entry
- model.ts: canonical resolver, default-model dispatch (1P -> 4.8,
  3P bumped to 4.7), display & marketing names
- modelOptions: getOpus48Option in PAYG 1P/3P, opusplan description
- modelCost: COST_TIER_5_25 pricing
- context: 1M-capable assertion
- prompts: FRONTIER_MODEL_NAME -> Opus 4.8
- integrations: hicap gateway, nearai brand/vendor/model entries
- claude brand catalog: new defineModel block

Tests: extends modelSupports1M coverage for 4.8.

* fix(models): gate Opus 4.8 out of PAYG 3P picker until rollout

Opus 4.8 was being added to the third-party (3P) model picker while
getDefaultOpusModel() keeps non-first-party usage on Opus 4.7. Remove the
3P option until 3P rollout is active; first-party picker is unaffected.

Addresses CodeRabbit review on #1769.

* test(integrations): cover NearAI anthropic/claude-opus-4-8 route

Adds focused regression coverage for the new Opus 4.8 provider/model
path: asserts the NearAI vendor catalog exposes the
anthropic/claude-opus-4-8 entry and that it resolves through its
modelDescriptorId to a registered NearAI model descriptor (vendor/brand
nearai, correct default model + label), plus the NearAI route base URL.
Addresses CodeRabbit's [Minor] request to test the exact route.

* fix(models): wire Opus 4.8 into adaptive thinking, 3P fallback, and knowledge cutoff

Addresses jatmn's review findings on #1769.

- [High] thinking.ts: add opus-4-8 to the adaptive-thinking allowlist.
  Without it, claude-opus-4-8 hit the generic opus exclusion and returned
  false, dropping first-party Opus 4.8 into budget-based thinking instead of
  thinking: { type: 'adaptive' }. Adds a regression test (provider mocked to
  a non-1P value so the allowlist is the only reason 4.8 returns true).
- [Medium] validateModel.ts: add an opus-4-8 -> opus47 entry to
  get3PFallbackSuggestion so an unavailable Opus 4.8 selection suggests 4.7.
- [Medium] prompts.ts: getKnowledgeCutoff now returns "January 2026" for
  claude-opus-4-8 and claude-opus-4-7 instead of falling through to the
  stale generic "January 2025".
- [Low] modelOptions.ts: update the PAYG 1P picker comment to include Opus 4.8.

The betas.ts structured-outputs / auto-mode allowlists are intentionally left
unchanged for this PR's scope (4.7 is also absent; auto mode is gated on PI
safety probes) — to be revisited with safety-research before enabling.

* fix(models): update remaining model-launch markers for Opus 4.8 default

Addresses jatmn's follow-up review on #1769 — markers missed when Opus 4.8
became the default.

- [P2] commitAttribution.ts: add explicit `opus-4-8` and `opus-4-7` branches to
  sanitizeModelName before the broad `opus-4` fallback, so commit/PR attribution
  shows the real model instead of `claude-opus-4`. Adds a focused regression
  test (commitAttribution.modelName.test.ts; mutation-checked).
- [P2] attribution.ts: update the unknown-first-party-model co-author fallback
  from 'Claude Opus 4.6' to 'Claude Opus 4.8', and the matching test expectation.
  Also fixed a sibling de-dup test that was passing only by coincidence (it hit
  the 4.6 fallback): point it at a model the public-name map actually recognizes
  (dot form) so it exercises the real prefix-dedup path.
- [P3] fastMode.ts: FAST_MODE_MODEL_DISPLAY 'Opus 4.6' -> 'Opus 4.8'.
- [P3] context.test.ts: update the stale modelSupports1M test title/comment from
  Opus 4.7 to 4.8 (the current first-party default).

* test(models): pin the claude-opus-4-7[1m] sanitizeModelName mapping too

CodeRabbit follow-up on #1769: the test covered the suffixed 4.8 path but not the
4.7 branch with the same [1m] session suffix. Add the claude-opus-4-7[1m] case so
both newly added mappings are pinned.

* fix(models): extend fast-mode + default-effort gates to the current default Opus

Addresses jatmn's review on #1769 — two predicates still gated to opus-4-6 only
while the default Opus is now 4.8.

- [P1] fastMode.ts: isFastModeSupportedByModel returned true only for opus-4-6,
  so for Max/Team Premium users on claude-opus-4-8 fast mode wouldn't actually
  enable even though FAST_MODE_MODEL_DISPLAY/the /fast command now say "Opus 4.8
  only". Extend the predicate to the fast-mode-capable Opus models (4.8/4.7/4.6).
- [P2] effort.ts: getDefaultEffortForModel applied the Pro/Max/Team `medium`
  default only for opus-4-6, so Pro/Max/Team sessions on the new default
  claude-opus-4-8 fell through to the generic effort path. Extend the branch to
  4.8/4.7/4.6 (per the @[MODEL LAUNCH] marker).

Adds regression tests for both (mutation-checked: reverting either predicate to
opus-4-6 only fails them).

* fix(models): wire Opus 4.8 into advisor, teammate fallback, skill vars, comments

Addresses jatmn's follow-up model-launch markers on #1769.

- [High] advisor.ts: modelSupportsAdvisor / isValidAdvisorModel only whitelisted
  opus-4-6 / sonnet-4-6, so first-party sessions on the new default
  claude-opus-4-8 reported the advisor tool unsupported. Add opus-4-8 and
  opus-4-7 to both (commands/advisor.ts and claude.ts use these centralized
  predicates, so they're covered). Adds a regression test (mutation-checked).
- [Medium] swarm/teammateModel.ts: getHardcodedTeammateModelFallback hardcoded
  CLAUDE_OPUS_4_6_CONFIG -> CLAUDE_OPUS_4_8_CONFIG, so new teammates spawn on the
  current default. Adds a first-party test case (mutation-checked).
- [Medium] skills/bundled/claudeApiContent.ts: SKILL_MODEL_VARS OPUS_ID/OPUS_NAME
  4.6 -> 4.8 (the bundled claude-api skill docs don't hardcode 4.6 elsewhere).
- [Low] effort.ts + figures.ts: refresh stale "max is Opus 4.6 only" comments to
  reflect the 4.8/4.7/4.6 runtime behavior.

* test(swarm): assert provider-aware teammate fallback for Bedrock too

CodeRabbit follow-up on #1769: add a non-first-party case so the provider-aware
fallback is covered. Bedrock resolves to the Opus 4.8 Bedrock model id.

* fix(models): give Opus 4.8/4.7 the elevated output-token limits and 3P fallback chain

Addresses jatmn's review on #1769.

- context.ts: getModelMaxOutputTokens only gave opus-4-6 the 64k/128k branch, so
  opus-4-7/4-8 fell through to the generic opus-4 branch and capped at 32k —
  including the new first-party default Opus 4.8. Extend the elevated branch to
  4.8/4.7/4.6. Adds a regression test (mutation-checked).
- errors.ts: get3PModelFallbackSuggestion had chains for opus-4-6/sonnet but not
  opus-4-8/4-7, so the error path suggested no fallback for the new default while
  validateModel.ts already does. Add opus-4-8 -> opus47 and opus-4-7 -> opus46 to
  mirror validateModel.ts.

* fix(models): allow structured outputs on Opus 4.8/4.7

Addresses jatmn's finding #2 on #1769. modelSupportsStructuredOutputs whitelisted
opus-4-1/4-5/4-6 but not 4-7/4-8, so first-party/Foundry requests on the new
default Opus 4.8 lost the structured-output support that 4.6 had. Add
claude-opus-4-7 and claude-opus-4-8 to the allowlist (4.6 supports it, so the
newer Opus models do too). Adds a first-party regression test (mutation-checked).

Auto-mode (modelSupportsExternalAutoMode) is intentionally left unchanged — it is
gated on separate safety review and was not part of this finding.

* fix(models): extend file-read mitigation exemption and effort callout to Opus 4.8/4.7

Addresses jatmn's remaining findings on #1769.

- [P2] FileReadTool.ts: MITIGATION_EXEMPT_MODELS only held claude-opus-4-6, so the
  new default claude-opus-4-8 got the cyber-risk reminder appended to every file
  read that 4.6 did not — a behavioral regression. Add claude-opus-4-8 and
  claude-opus-4-7 so the recent Opus models inherit 4.6's exemption.
- [P3] EffortCallout.tsx: shouldShowEffortCallout gated the medium-effort-default
  notification to opus-4-6 only; the same default now applies to opus-4-8, so
  users on the new default never saw it. Extend the gate to 4.8/4.7/4.6. Adds a
  regression test (mutation-checked).

* fix(models): resolve Opus 4.6→4.8 drift in cost tracking, notifications, and picker strings

Addresses jatmn's review on #1769 — remaining model-launch drift now that the
first-party default is Opus 4.8.

- [P1] modelCost.ts: getModelCosts only applied the elevated fast-mode tier to
  opus-4-6, so fast-mode Opus 4.8 was billed at the normal COST_TIER_5_25 rate
  while the picker advertised the fast-mode $30/$150 price. Extend the fast-mode
  cost check to the fast-mode-capable Opus models (4.8/4.7/4.6). Non-fast usage is
  unchanged (all three already map to COST_TIER_5_25). Adds a regression test
  (mutation-checked).
- [P2] useModelMigrationNotifications.tsx: "Model updated to Opus 4.6" -> 4.8
  (the migration lands users on the opus alias = 4.8 for first party).
- [P2] commands/model/model.tsx: the 1M-unavailable error said "Opus 4.6"; made
  it generic ("Opus with 1M context...") since the gate matches any opus[1m].
- [P2] modelOptions.ts: getOpus46_1MOption is now provider-aware (3P → Opus 4.6,
  first-party → Opus 4.8); getMaxOpus46_1MOption (always first-party) → Opus 4.8.
- [P3] migrateLegacyOpusToCurrent.ts: corrected the stale comment (opus alias
  resolves to 4.8, not 4.6).

* docs(notifs): correct Opus default comment to 4.8 for 1P

Comment said 4.6 but the migration notification text and the opus alias both resolve to Opus 4.8 for first-party users. Addresses jatmn P3 review note.

* fix(integrations): remove duplicate claude-opus-4-8 descriptor

A second claude-opus-4-8 entry (vendorId anthropic) with downgraded 200k/8192 specs duplicated the canonical 1M/128k descriptor. The artifact generator rejects duplicate (id, vendorId) pairs, so integrations:generate failed and smoke-and-tests could not pass. Removed the duplicate; the canonical entry and checked-in generated artifacts are unchanged. Addresses jatmn P1.

* fix(models): address Opus 4.8 review — extra-usage label, callout test, stale copy

- isBilledAsExtraUsage: recognize opus-4-7/4-8 1M variants, not just 4.6, so the
  "Billed as extra usage" label shows for the new default and 3P default
- EffortCallout modelGate test: drop the unreliable `?ts=` cache-busting import
  and rely on mock.module live bindings, so the gate runs against the mocked
  deps on Linux CI (where the query-tagged specifier was not re-evaluated)
- refresh stale "Opus 4.6+" effort help text and callout comments to reflect the
  recent Opus models (4.8/4.7/4.6) the gate now covers

* test(effort): make Opus 4.8 callout regression deterministic via pure predicate

The behavioral test mocked auth/config/effort and relied on the already-evaluated
EffortCallout picking up those mocks, which is order-dependent and failed only in
the full Linux CI suite (both the `?ts=` dynamic-import and the static-import
variants regressed there). Extract the model check as a pure exported
`effortCalloutCoversModel` and assert it directly with no module mocking, so the
#1769 regression is covered deterministically on every platform.

* test(effort): drop config-dependent 'opus' alias from callout regression test

The bare 'opus' alias routes through getDefaultOpusModel(), whose result is
environment/config-dependent, so `effortCalloutCoversModel('opus')` was false in a
clean Linux CI environment even though the gate logic is correct — that single
assertion was the only failure in smoke-and-tests (the explicit-id assertions
passed). Assert the gate's opus-4-8/4-7/4-6 coverage with explicit canonical model
ids (incl. a [1m] variant) instead, which is deterministic on every platform.
2026-06-26 21:44:08 +08:00
euxaristiaandGitHub 701b68c215 fix(query): prevent spurious Windows interruption prompt by passing 'interrupt' reason (#1733)
* fix(query): prevent spurious Windows interruption prompt by passing 'interrupt' reason

* fix(cli): pass 'interrupt' abort reason and add stop-hook regression tests

* test(query): exercise handleStopHooks abort-reason branch directly

Adds focused coverage that imports and runs handleStopHooks() while a
Stop-hook generator is being consumed, asserting abort('interrupt')
suppresses the synthetic '[Request interrupted by user]' message and a
default abort still yields it. Closes jatmn's P2 regression request.
2026-06-25 06:28:57 +08:00
euxaristiaandGitHub 669ecdfa8b fix: prevent recursive debounce infinite loop in team memory sync (#1726)
* fix: prevent recursive debounce infinite loop in team memory sync

* fix: chain cap-reschedule push after in-flight promise instead of running concurrently

* fix: preserve currentPushPromise when replaced during yield point + tests

* fix: clear pending debounce timer in _resetWatcherStateForTesting

* fix(teamMemorySync): prevent concurrent pushes, make clearing identity-safe, and avoid duplicate follow-ups

* fix(teamMemorySync): guard capped follow-up push against suppression; fix test gaps

Addresses jatmn's three P3 findings on #1726.

- watcher.ts: the capped reschedule path queues a serialized follow-up
  executePush() without consulting pushSuppressedReason, whereas schedulePush()
  short-circuits on suppression. If a permanent failure set suppression while
  the in-flight push was running, the queued follow-up fired one redundant,
  identically-failing call. Skip executePush() when pushSuppressedReason is set,
  mirroring schedulePush(). Adds a regression test (mutation-checked: removing
  the guard makes it fail).

- watcher.test.ts: the "resets to 0 when executePush completes" test was
  vacuous — rescheduleCount started at 0 and executePush() never owns that
  reset (onDebounceFire and _resetWatcherStateForTesting do). Renamed to
  "clears pushInProgress when executePush completes" and dropped the misleading
  rescheduleCount assertion; the reset path stays covered by the onDebounceFire
  cap test.

- watcher.test.ts: the top-level mock.module('./index.js') is process-global and
  mock.restore() does not undo it. Follow the spawnCtxAgent.test.ts pattern —
  (re)register the mock in beforeEach and restore the real module in afterEach —
  so it can't silently bleed into a future test that imports teamMemorySync/index.
2026-06-25 06:27:49 +08:00
euxaristiaandGitHub 02d43b6942 fix: surface swallowed error in plan file write (#1725)
* fix: surface swallowed error in plan file write

* test: add regression test for plan file write failure path

* test(ExitPlanModeV2Tool): scope fs mock to single test and pass full call signature

Two CodeRabbit concerns:

1. The global fs/promises mock in beforeAll was leaking into unrelated
   test suites (e.g. loadAgentsDir.test.ts) because mock.module() cannot
   be cleanly undone mid-suite. Move the mock into the single test that
   needs it and call mock.restore() in a finally block so it is torn
   down as soon as the assertion completes.

2. ExitPlanModeV2Tool.call() was invoked with 2 args but the Tool base
   class signature requires 4 (input, context, canUseTool,
   parentMessage). Pass the two extra args so the test typechecks
   against the real signature.

* fix(test): prevent fs/promises mock leak from ExitPlanModeV2Tool test

Addresses jatmn's P2 on #1725.

The previous test used mock.module('fs/promises', ...) in a try/finally
with mock.restore(). On Linux CI the mock was still active when
loadAgentsDir.test.ts ran, causing all five agent-fixture tests to fail
with "write failed" traces pointing back to this file.

Fix: add afterEach(mock.restore()) to guarantee the mock is torn down
after each test, regardless of pass/fail path. Verified locally by
running both test files in the same process — 6/6 pass.

* fix(test): avoid fs/promises mock entirely to prevent CI leak

The afterEach(mock.restore()) approach still leaked the fs/promises mock
into loadAgentsDir.test.ts on Linux CI (CodeRabbit comment at 12:12 EST).
Bun's mock.module on Linux replaces the module cache in a way that
persists across test files in the same process.

Fix: don't mock fs/promises at all. Instead, re-mock plans.js to point
getPlanFilePath at a nonexistent directory, so the REAL fs/promises.writeFile
rejects with ENOENT. Nothing to leak.

Verified: both test files pass in both orderings with --max-concurrency=1.

* Fix race on exiting plan mode by saving plan file before permission updates and add regression tests asserting no side effects on error

* fix(test): completely isolate mock module by using teammate context APIs instead of module mock override

* test(plan-mode): cover the React write-before-permission guard; drop global fs/promises mock

Addresses jatmn's two P2 findings on #1725.

- Extract the plan-file write guard from ExitPlanModePermissionRequest's
  handleResponse into an exported persistPlanFileBeforeExit() helper (jatmn
  suggested extraction for testability). The component path is unchanged: on
  write failure it stays in plan mode (returns early) and queues a
  'plan-save-error' notification. Adds focused tests for both success and
  write-failure (mutation-checked: a helper that swallows the failure fails the
  test). Tests use real filesystem paths — a temp file for success and a path
  with a missing parent dir for failure — so they need no fs/promises mock.

- ExitPlanModeV2Tool.test.ts: replace the process-global mock.module('fs/promises')
  in both write-failure tests with the same real-failing-path mechanism (a plan
  path whose parent dir does not exist → genuine ENOENT). This removes the
  fragile core-module mock jatmn flagged, which mock.restore() cannot undo and
  which could leak into other test files.

* test(plan-mode): assert the specific ENOENT write failure in ExitPlanModeV2Tool tests

Addresses CodeRabbit's review on #1725: the two write-failure tests asserted a
generic rejects.toThrow(), which would pass on any error. Tighten both to
rejects.toThrow(/ENOENT/) so they lock in the intended missing-parent-dir write
failure rather than masking an unrelated error. The post-write side-effect
assertions (persistFileSnapshotIfRemote / writeToMailbox / setAppState not
called) are unchanged.

* test(plan-mode): add rendered guard test for handleResponse write failure

Addresses jatmn's P3 on #1725: the only coverage for the write-before-permission
guard was at the persistPlanFileBeforeExit helper level; there was no test that
handleResponse itself returns early on write failure.

Renders ExitPlanModePermissionRequest (following the MonitorPermissionRequest
harness), points the V2 plan file at a path whose parent dir is missing (real
ENOENT), confirms the first accept option, and asserts the plan-save-error
notification is queued while toolUseConfirm.onAllow / onReject and onDone are NOT
called. Mutation-checked: removing the `if (!saved) return` guard makes it fail,
so a future refactor cannot silently drop the guard.

* fix(test): type addNotification mock so render test typechecks

CodeRabbit (correctly) flagged that `mock(() => {})` infers a zero-arg tuple, so
`call[0]` was TS2493 and `bun run typecheck` (CI) failed. Type the mock param
with the real `Notification` type, which also lets the assertion drop its cast.
2026-06-25 06:25:58 +08:00
euxaristiaandGitHub adcf5e5839 fix(permissions): bound the speculativeChecks cache with FIFO eviction (#1724)
Internal/upstream defensive maintenance: cap the speculativeChecks Map at
MAX_SPECULATIVE_CHECKS_SIZE (1000) and FIFO-evict the oldest entries after each
insert, so the bash-classifier speculative cache can't grow unbounded when the
classifier path is active (it is a stub in the open-source build; this guards
the upstream build where it is reachable).

Pure cache-bounding — no runtime or permission behavior change, and no new env
vars. Adds focused FIFO-eviction regression tests via a small `_test` surface
(mutation-checked: neutering eviction fails them). Rebased onto current main.
2026-06-25 06:25:00 +08:00
euxaristiaandGitHub 38b0e27333 fix(opencode-go): sync model catalog with opencode.ai/go (#1745)
* fix(opencode-go): sync model catalog with opencode.ai/go

The OpenCode Go subscription page (https://opencode.ai/go) lists 13
models, but the catalog had 20. Remove the 7 models no longer offered:
glm-5, kimi-k2.5, minimax-m2.5, qwen3.5-plus, mimo-v2-pro,
mimo-v2-omni, hy3-preview.

Catalog now matches the page exactly:
- OpenAI-compatible: GLM 5.2, GLM 5.1, Kimi K2.7 Code, Kimi K2.6,
  DeepSeek V4 Pro, DeepSeek V4 Flash, MiMo V2.5 Pro, MiMo V2.5
- Anthropic messages: MiniMax M3, MiniMax M2.7, Qwen3.7 Max,
  Qwen3.7 Plus, Qwen3.6 Plus

Updates gateway catalog, model descriptors, generated artifacts, and
tests.

* fix(opencode-go): reorder model catalog to match opencode.ai/go listing

Reorder both the gateway catalog and model descriptor lists to match the
order models appear on https://opencode.ai/go, so the in-app model picker
mirrors the subscription page. No model added or removed — purely a
reorder.

GLM-5.2 → Qwen3.7 Max → Kimi K2.7 Code → MiMo V2.5 Pro → DeepSeek V4 Pro
→ Qwen3.7 Plus → MiniMax M3 → MiMo V2.5 → DeepSeek V4 Flash → GLM 5.1
→ Kimi K2.6 → Qwen3.6 Plus → MiniMax M2.7

* test(opencode-go): assert exact model set matches opencode.ai/go catalog

Address CodeRabbit review on #1745 — the count-only test wouldn't catch
catalog drift. Add a strict set assertion verifying the 13 expected IDs
are present and no removed/unexpected IDs remain.

* test(opencode-go): update Anthropic Messages route test for refreshed catalog

The direct-env-routing test listed minimax-m2.5 and qwen3.5-plus, which
were removed from the opencode-go catalog. Replace with the five
/messages-endpoint models that remain: minimax-m3, minimax-m2.7,
qwen3.7-max, qwen3.7-plus, qwen3.6-plus.

Unblocks the smoke-and-tests CI check on #1745.

* fix: update OpenCode Go model references to 13 models and add assertions
2026-06-23 09:30:08 +08:00
euxaristiaandGitHub aed42df19b fix: treat 5xx HTML overload pages as retryable provider_unavailable (#1750)
Reorders classifyOpenAIHttpFailure so the status >= 500 branch runs before
the isMalformedProviderResponse check. Gateway 502/504 overload pages have
HTML bodies (matching "<!doctype html" / "<html") that previously tripped
the malformed-provider-response path, marking the error non-retryable and
surfacing "Provider returned a malformed response" even though the failure
was transient — users had to retry manually.

Now any 5xx is classified as provider_unavailable (retryable) regardless of
body shape, matching the actual semantics. 4xx HTML responses still classify
as malformed_provider_response since those are genuine protocol failures.

Adds three regression tests covering 502 HTML, 504 HTML, and the unchanged
400 HTML path.
2026-06-23 07:51:10 +08:00
a02c44143b fix(web-search): close SSRF bypasses in custom provider hostname guard (#610)
The previous `isPrivateHostname` used a list of regexes against
`URL.hostname`. Several literal-address forms slipped past it:

- IPv4-mapped IPv6 `[::ffff:127.0.0.1]` (WHATWG URL normalizes to
  `[::ffff:7f00:1]`, which no regex matched) — lets callers reach
  loopback and other private v4 via an IPv6 literal.
- ULA `fc00::/7` (e.g. `[fc00::1]`) — not covered.
- Link-local `fe80::/10` (e.g. `[fe80::1]`) — not covered.
- IPv4 `169.254.0.0/16` (cloud metadata, including 169.254.169.254),
  `100.64.0.0/10` (CGNAT), and the full `0.0.0.0/8` — not covered.
- The IPv6 regex `/^\[::1?\]$/` also required brackets, but `URL.hostname`
  returns bracketed form anyway, so this part happened to work.

WHATWG `new URL(...)` already normalizes short-form / numeric / hex /
octal IPv4 to dotted-quad before we see it, so those cases were in fact
handled — the remaining gaps were IPv6 and a few missing v4 ranges.

Replace the regex list with:
- a dotted-quad IPv4 parser + int range check covering 0/8, 10/8,
  100.64/10, 127/8, 169.254/16, 172.16/12, 192.168/16;
- a small IPv6 parser (handles `::` compression and embedded v4 suffix)
  + a byte-range check covering `::`, `::1`, IPv4-mapped (recursing
  into the v4 classifier), IPv4-compatible, `fc00::/7`, `fe80::/10`,
  and `fec0::/10`.

Export `isPrivateHostname` and add unit tests covering every bypass
listed above plus public-address negatives.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 21:09:46 +08:00
7817fe88bd fix(web-search): stop leaking abort listeners in custom provider retry (#611)
`fetchWithRetry` created a fresh `AbortController` per attempt and did:

    signal?.addEventListener('abort', () => controller.abort(), { once: true })

The listener was never removed. Consequences:

- On retry, a second listener was attached to the caller's signal,
  each closing over a different controller.
- After a successful fetch, the listener remained on the caller's
  signal indefinitely, referencing a controller whose work was done.
  For a long-lived caller signal this is a slow leak.
- The `{ once: true }` only helps if the signal actually fires — on
  non-aborted signals the listener stays attached forever.

Replace the manual controller + timer + listener dance with
`AbortSignal.any([signal, AbortSignal.timeout(ms)])`, which the
codebase already uses elsewhere (see src/services/mcp/xaa.ts). This:

- has no user-code listener to leak,
- gives each attempt a fresh independent timeout,
- cleanly distinguishes caller-initiated abort from timeout via
  `signal.aborted` vs `timeoutSignal.aborted` before rewriting the
  error as "Custom search timed out after Ns".

Also resets `lastStatus` per attempt so a 5xx on attempt 0 can't leak
into attempt 1's retry decision, and collapses the two redundant
retry branches (`lastStatus >= 500` and `lastStatus === undefined`)
into one.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 19:37:08 +08:00
euxaristiaandGitHub b126e38b1a fix: display selected model in startup screen instead of hardcoded sonnet 4.6 (#587) 2026-04-11 21:20:00 +08:00