feat(models): category-based cost policy + live catalog/pricing refresh (#1080/#1183)#1445
Conversation
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| Status | Check | Details |
|---|---|---|
| ❌ | Single commit | 5 commits — consider squashing before review |
| ✅ | Not in draft | Ready for review |
| ❌ | Branch up to date | dev is 8 commit(s) ahead — rebase recommended |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | 0 active Copilot thread(s) resolved (2 outdated skipped) |
| ❌ | CI passing | 6 check(s) still running |
Files Changed (13 files, +1632 −7)
| File | +/− |
|---|---|
.changeset/feat-cost-policy-1080.md |
+27 −0 |
packages/squad-cli/src/cli-entry.ts |
+6 −0 |
packages/squad-cli/src/cli/commands/models.ts |
+364 −0 |
packages/squad-sdk/src/agents/index.ts |
+5 −0 |
packages/squad-sdk/src/agents/lifecycle.ts |
+69 −1 |
packages/squad-sdk/src/agents/model-selector.ts |
+219 −4 |
packages/squad-sdk/src/config/models.ts |
+59 −0 |
packages/squad-sdk/src/config/schema.ts |
+4 −0 |
packages/squad-sdk/src/runtime/config.ts |
+22 −0 |
test/config-schema.test.ts |
+36 −0 |
test/lifecycle.test.ts |
+109 −2 |
test/model-selector-policy.test.ts |
+161 −0 |
test/models-refresh.test.ts |
+551 −0 |
Total: +1632 −7
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
🟡 Impact Analysis — PR #1445Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (1 file)
squad-cli (2 files)
squad-sdk (6 files)
tests (4 files)
|
3d61719 to
66f1c68
Compare
|
@primetimetank21 — flagging a synergy with your catalog refresh: PR #1448 (per-agent context tier: |
@tamirdresher -- I tried the command locally and it worked! Here's what I ran (path scrubbed): I'll also try this on #1444. |
|
From Earl's Squad: @tamirdresher — tried it on #1444 (the catalog-refresh branch). Merged current
Tier resolution stays dynamic (inferred per-model from |
…nrichment Introduce an opt-in GitHub billing cost-ceiling policy (lightweight/versatile/ powerful) as a separate axis from the quality ModelTier. Resolution is finalized against the effective policy: implicit over-ceiling picks are downgraded deterministically, explicit overrides are honored with a loud warning, and the outcome is surfaced via the event bus + logs instead of being swallowed (the defect that stalled the prior attempt). When no in-ceiling model exists the policy fails closed and loud; unknown/out-of-catalog IDs pass through unchanged. Wire the policy end-to-end through AgentLifecycleManager.spawnAgent and add a squad models refresh diagnostic that reconciles the committed seed catalog against live sources: the canonical Copilot models API (optional, via gh auth token) with a graceful auth-free fallback to the public github/docs pricing YAML, writing to the gitignored local cache. On the authenticated happy path, refresh also fetches the docs YAML and enriches the API-discovered catalog with pricing (and releaseStatus) joined by model id. The API stays authoritative for id/category/reachability; docs only supplies pricing for ids the API already returned (no ids added). Enrichment is best-effort and fail-open, keeping source: 'api'. DOCS_NAME_TO_ID is keyed by lowercased spaced display names and trailing footnote markers are stripped so all 13 seed ids join their pricing when present. Pricing remains diagnostic-only and is not wired into policy enforcement. Refs bradygaster#1080 Refs bradygaster#1183 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
66f1c68 to
48e25d2
Compare
…r pricing enrichment DOCS_NAME_TO_ID had no entries for the three new gpt-5.6 models, so the docs YAML pricing join silently skipped them (unmatched names are intentionally skipped by design). The display names 'GPT-5.6 Luna/Sol/Terra' (proper-noun word suffixes) do not follow the same pattern as 'GPT-5.5'/'GPT-5.4 mini', so adding them requires explicit map entries. Added entries: 'gpt-5.6 luna' -> 'gpt-5.6-luna' 'gpt-5.6 sol' -> 'gpt-5.6-sol' 'gpt-5.6 terra'-> 'gpt-5.6-terra' TDD: new parseDocsYaml test in test/models-refresh.test.ts exercises the full two-row YAML fixture (Default + Long context rows) for all three IDs and asserts correct id mapping, githubCategory, pricing, and releaseStatus. RED -> GREEN. Verified live: squad models refresh now enriches all three with pricing and releaseStatus from the docs YAML. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…orithmic normalization + unpriced warning
PROBLEM: DOCS_NAME_TO_ID was a 16-entry hand-maintained allow-list that required
per-model edits for every new catalog entry. Silent skip (if (!id) continue) meant
brand-new models from the API would silently get no pricing without any signal.
SOLUTION:
- Export normalizeDisplayName(name): strips footnote markers [^...], strips
parentheticals (...), lowercases, collapses whitespace to hyphens. Handles
all current docs display names algorithmically with zero overrides needed:
'GPT-5.6 Luna' -> 'gpt-5.6-luna'
'Claude Sonnet 5[^promo]' -> 'claude-sonnet-5'
'Claude Opus 4.8 (fast)' -> 'claude-opus-4.8'
'Gemini 2.5 Pro' -> 'gemini-2.5-pro'
- Replace DOCS_NAME_TO_ID with empty DOCS_NAME_OVERRIDES escape hatch (zero
current entries; mechanism retained for genuine edge cases).
- docs-fallback path now filters by seedIdSet so extra docs rows (Fable 5,
Kimi, etc.) remain harmlessly ignored — catalog-driven behavior preserved.
- Add unpricedIds: string[] to RefreshResult: catalog models discovered from
API/docs but with no pricing. Printed as a loud warning in refresh output.
New/unpriced models are visible signal, not silent drift.
TDD: 4 new normalizeDisplayName unit tests + 3 unpricedIds tests -> RED then GREEN.
21/21 models-refresh tests pass. 178/178 targeted tests pass. Full suite: same
5 pre-existing EBUSY failures as baseline, no regressions.
Live: gpt-5.6-luna/sol/terra all enrich with pricing; warning correctly surfaces
gemini-3.1-pro-preview and mai-code-1-flash-picker as not-yet-in-docs models.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…vent SKU collision
REGRESSION introduced in previous commit: normalizeDisplayName stripped entire
parenthetical groups (e.g. '(fast mode) (preview)'), so 'Claude Opus 4.8' and
'Claude Opus 4.8 (fast mode) (preview)' both normalized to 'claude-opus-4.8'.
Last-wins in the enrichment Map picked the fast-mode row (/\) instead of
the real model row (/\).
ROOT INSIGHT: a parenthetical qualifier denotes a DIFFERENT product SKU. Stripping
it causes a pricing collision. Correct fix: strip the '(' and ')' chars but KEEP
the words inside, so the fast-mode row normalizes to
'claude-opus-4.8-fast-mode-preview' — a non-catalog id that enrichWithPricing
ignores, leaving the base-model pricing intact.
Footnote stripping ([^...]) is kept: footnotes are promo markers on the SAME
model ('Claude Sonnet 5[^promo]' -> 'claude-sonnet-5'), not distinct SKUs.
Scan confirmed: only ONE parenthetical row in the live docs YAML ('Claude Opus 4.8
(fast mode) (preview)'). All other models are unaffected.
Live: claude-opus-4.8 = \.00/\.00 (correct), all 16 models enriched, no
unpriced warning.
TDD: updated normalizeDisplayName unit test (parens-kept behavior) + 2 new
collision-protection tests (parseDocsYaml + enrichWithPricing). RED -> GREEN.
180/180 targeted tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…x Ralph free-model wording (#1469) * docs/models: prefer newest per series (Opus 4.8 + Sonnet 5), add GPT-5.6 IDs, fix Ralph free-model wording Follow-up to #1444 (merged), addressing tamirdresher's three explicit requests in #1444 (comment) (refs #1080, #1183). Item 1 — Prefer newest model per series (fallback ordering): - packages/squad-sdk/src/config/models.ts: DEFAULT_FALLBACK_CHAINS.standard now leads with claude-sonnet-5 (was claude-sonnet-4.6). - packages/squad-sdk/src/runtime/constants.ts: MODELS.FALLBACK_CHAINS.standard same reorder. Premium chains already led with claude-opus-4.8 post-#1444. - model-selection-reference.md, SKILL.md files: examples now show claude-opus-4.8 first in Premium, claude-sonnet-5 first in Standard. Item 2 — Add GPT-5.6 model IDs (gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna): - Added to MODEL_CATALOG in models.ts (tier: standard, githubCategory: powerful — mirrors gpt-5.5 sibling entry). - Added to DEFAULT_FALLBACK_CHAINS.standard and MODELS.FALLBACK_CHAINS.standard (after claude-sonnet-4.6, before gpt-5.4). - Reachability: CLI-observed as Standard-tier reachable models (2026-07-13). - NOTE: gpt-5.6 entries in DOCS_NAME_TO_ID (cli/commands/models.ts) are deferred until draft PR #1445 merges (that file is in #1445's diff). - Updated Valid Models in all model-selection-reference.md + SKILL.md copies. Item 3 — Ralph circuit-breaker free-model wording: - .squad-templates/ralph-circuit-breaker.md: replaced multiplier table (0x / "Free — unlimited") with usage-based framing (lightweight category — lowest-cost, still billed). Updated preferredModel example to claude-sonnet-5. - Synced to all 3 template targets via sync-templates.mjs. NOTE: MODELS.DEFAULT (claude-sonnet-4.6) is intentionally left unchanged. That global default is out of scope for this PR — can be a separate decision. TDD: test/catalog-refresh.test.ts — wrote failing tests first (8 RED), then implemented changes. Final result: 16/16 tests GREEN. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(models): sync full Standard chain in doc/skill examples; add changeset Address Copilot review comments on PR #1469: - 3 doc/skill files had abbreviated Standard fallback chain (omitting gpt-5.6-terra, gpt-5.6-luna, gemini-2.5-pro). Now matches the full SDK runtime chain exactly: claude-sonnet-5 → claude-sonnet-4.6 → gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.4 → gpt-5.3-codex → claude-sonnet-4.5 → gemini-2.5-pro → (omit model param) Files: .squad-templates/model-selection-reference.md, .copilot/skills/model-selection/SKILL.md (manual), .squad/skills/model-selection/SKILL.md (sync canonical) Synced to all 3 template targets + 2 skill package targets. - Premium chain confirmed correct (claude-opus-4.8 first) — no change needed. - Add .changeset/models-gpt56-fallback-ordering.md (@bradygaster/squad-sdk patch) to satisfy Changelog Gate CI check. 16/16 catalog-refresh tests remain GREEN. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: update stale standard-chain[0] assertions to claude-sonnet-5 Three test files hard-coded the old fallback chain head (claude-sonnet-4.6) that was intentionally reordered in the parent commit (newest-per-series first, tamirdresher PR #1444 follow-up). The tests remain logically correct — they now assert the NEW intended behavior. - test/compat-v041.test.ts: standard[0] → 'claude-sonnet-5' - test/models.test.ts: standard[0] → 'claude-sonnet-5' - test/agents.test.ts: standard fallback chain toEqual updated (reordered + added gpt-5.6-sol/terra/luna) Inline comment added to each noting the deliberate reorder. Pre-existing failures (cli-packaging-smoke, init-scaffolding) are EBUSY file-lock errors unrelated to these changes — confirmed on baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(models): correct gpt-5.6 billing categories to match live Copilot API Live API (canonical) returns: sol=powerful, terra=versatile, luna=lightweight. Original seed incorrectly set all three to 'powerful' (mirroring gpt-5.5). Tier (standard) and fallback-chain membership are unchanged; only githubCategory (the cost-policy billing axis) is corrected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a category-based cost ceiling policy to SDK model selection (wired through lifecycle with surfaced outcomes) and introduces a squad models refresh CLI diagnostic that reconciles the seed model catalog against live sources and enriches with docs pricing (cached locally).
Changes:
- Implement cost-ceiling policy finalization in model selection (downgrade/prune/warn) and expose the outcome on the resolved model.
- Wire cost policy through
AgentLifecycleManager.spawnAgentand surface policy actions viaconsole.warn+EventBus. - Add
squad models refreshcommand (API-first with docs fallback), cache output to.squad/.cache/models.json, and add comprehensive tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/models-refresh.test.ts | Adds unit/integration coverage for refresh sourcing, enrichment, normalization, and warnings. |
| test/model-selector-policy.test.ts | Adds cost policy behavior tests (downgrade, warn-allow, pruning, fail-closed signaling). |
| test/lifecycle.test.ts | Adds end-to-end lifecycle wiring tests for cost policy + event/warn surfacing. |
| test/config-schema.test.ts | Adds runtime config validation tests for models.costPolicy.maxCategory. |
| packages/squad-sdk/src/runtime/config.ts | Extends runtime config model selection schema + validation for costPolicy. |
| packages/squad-sdk/src/config/schema.ts | Adds costPolicy to typed config surface. |
| packages/squad-sdk/src/config/models.ts | Introduces cost policy types/outcome and category ordering helpers. |
| packages/squad-sdk/src/agents/model-selector.ts | Adds effective policy merge, chain pruning, deterministic downgrade, and policy outcomes. |
| packages/squad-sdk/src/agents/lifecycle.ts | Threads persistent + session policy through resolution and emits policy outcomes. |
| packages/squad-sdk/src/agents/index.ts | Exports new policy helpers/types for external consumption/tests. |
| packages/squad-cli/src/cli/commands/models.ts | Implements squad models refresh (API + docs YAML parsing/enrichment) and caching. |
| packages/squad-cli/src/cli-entry.ts | Adds CLI routing for the new models command. |
| .changeset/feat-cost-policy-1080.md | Declares minor bumps for SDK/CLI with a combined changelog entry. |
…tier pricing, suppress spurious unpriced warning Comment 1 (parseDocsYaml): skip `tier: Long context` rows so enrichWithPricing always attaches the Default-tier (base) price per model, not the higher Long-context threshold price. Two-row models (gpt-5.5, gpt-5.4, gpt-5.6-*) now correctly show their Default-tier pricing. Comment 2 (unpricedIds): track whether docs enrichment actually produced any priced result (enrichmentRan); only compute/emit unpricedIds when enrichmentRan is true. Suppresses the misleading "all models unpriced" warning when the docs YAML fetch itself fails — distinguishes enrichment failure from genuinely new unpriced models. Tests: 4 new RED-first tests (Default-tier-wins, enrichWithPricing Default price, docs-fetch-throws suppression, empty-yaml suppression) → 27/27 GREEN. Refs bradygaster#1080 / bradygaster#1183 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
From Earl's Squad: @tamirdresher — requesting your review on the category-based cost policy + This is a separate feature continuing the broader cost-policy direction: it adds a The PR partially advances issues #1080 and #1183 (tracked in both issue threads with a breakdown of what's proposed vs. what remains). It does not close either issue — follow-up work is noted. Happy to address any feedback. |
What & why
Adds a category-based cost policy for model selection (GitHub AI Credits billing) and a
squad models refreshdiagnostic that reconciles the committed seed catalog against live sources — now carrying up-to-date pricing alongside the live catalog.Refs #1080
Refs #1183
Changes
packages/squad-sdk/src/agents/model-selector.ts):buildEffectiveCostPolicy+finalizeResolvedModel(downgrade / prune / fail-closed). Two independent axes kept separate — quality tier (premium/standard/fast) vs. GitHub billing category (lightweight/versatile/powerful). No hardcoded pricing.packages/squad-sdk/src/agents/lifecycle.ts): threadsconfig+ session cost policy intoresolveModel, and surfaces the outcome loud (EventBus +console.warn) — fail-closed when no compliant model exists. (Addresses the "dead code" gap from the prior feat: add cost policy + refresh model catalog for GitHub AI Credits billing #1089 approach.)squad models refresh(packages/squad-cli/src/cli/commands/models.ts): auth-free-first hybrid sourcing — canonical Copilot models API (viagh auth token,Copilot-Integration-Id: copilot-cli) for the live catalog + category, enriched on the API path with pricing/release_statusfrom the publicgithub/docsmodels-and-pricing.yml. Display names are normalized algorithmically to catalog ids vianormalizeDisplayName— no hand-maintained allow-list, so new models enrich automatically. Parenthetical qualifiers (e.g.(fast mode) (preview)) are kept in the normalized id to prevent pricing collisions between distinct SKUs sharing a base name (e.g.claude-opus-4.8vs.claude-opus-4.8-fast-mode-preview).tier: Long contextthreshold rows in the docs YAML are skipped so the Default tier (lower base price) is always used — prevents the long-context row overwriting the canonical per-call pricing. Models with no docs pricing entry emit a visible⚠ N catalog model(s) have no pricing from docs: <ids>warning rather than silently vanishing; the warning is suppressed when the docs fetch itself fails (avoids misleading "all models unpriced" noise from a network error). Best-effort / fail-open (a docs failure never hard-fails the refresh). Discovered ids are written only to the gitignored.squad/.cache/models.json— never to committed files.Testing
TDD throughout (red → green). Targeted suites:
test/models-refresh.test.ts(27, including normalization unit tests, parenthetical SKU collision-protection tests, Default-tier-wins tests, and docs-fetch-failure suppression tests),test/model-selector-policy.test.ts(11, covering AC5 implicit downgrade, AC6 explicit warn-allow, AC7 chain pruning, AC8 fail-closed, AC9 empty-pruned-chain, AC10 out-of-catalog passthrough, AC11 lifecycle wiring),test/catalog-refresh.test.ts(7). CLI package typecheck clean.Verified against live endpoints:
source: api, 19 models discovered, 17/19 enriched with current pricing;gemini-3.1-pro-previewandmai-code-1-flash-pickerare new API models not yet in the docs pricing YAML — correctly surfaced by the unpriced warning rather than silently dropped.claude-opus-4.8correctly shows $5.00/$25.00 (Default tier). Two-row models (gpt-5.4/5.5/5.6-*) now show Default-tier base pricing — no longer inadvertently showing the higherLong contextrow price.📊 Live catalog + pricing (19 models, Default-tier)
Categories come from the Copilot models API (canonical); pricing +
release_statusare joined by id from the publicgithub/docsmodels-and-pricing.yml(Default tier only — Long context rows are skipped). Pricing is diagnostic/reconciliation data written to the gitignored cache — it is not wired into policy enforcement.gemini-3.1-pro-previewandmai-code-1-flash-pickerare correctly reported as unpriced (no docs entry); no warning fires on network/fetch failure.Changeset included (
.changeset/feat-cost-policy-1080.md— covers both features,minorbump for@bradygaster/squad-sdkand@bradygaster/squad-cli).🧪 How to test locally (manual + agent)
How to test locally
All commands are PowerShell-safe. Run from the repo root (
C:\...\squad).Feature 1 —
squad models refreshStep 1: Build both packages
Step 2: Run the refresh (requires
gh auth statusto show an active login for the Copilot API path; falls back to public docs YAML automatically if unauthenticated)Expected output:
Source: Copilot models API (canonical),Discovered: 19 model(s), gpt-5.6-luna/sol/terra in theAddedlist (first run).gemini-3.1-pro-previewandmai-code-1-flash-pickerare new API models with no docs pricing yet — the refresh reports⚠ 2 catalog model(s) have no pricing from docs: gemini-3.1-pro-preview, mai-code-1-flash-picker. If the docs YAML fetch itself fails, no unpriced warning fires.Step 3: Verify the key prices (writes/reads/removes a temp .cjs — avoids ESM import complexity and PS backtick escaping)
Expected (Default-tier base prices):
Step 4: Confirm the working tree stays clean
git status --short.squad/.cache/models.jsonis gitignored and will not appear. Any pre-existing untracked noise (.gitattributes,.mcp.json) is unrelated to this PR.Step 5: Run the models-refresh unit tests
Expected:
Tests 27 passed (27)— includes normalization unit tests, parenthetical SKU collision-protection tests, Default-tier-wins tests, and docs-fetch-failure suppression tests.Feature 2 — Category-based cost policy
The cost policy is an SDK feature (
resolveModel+buildEffectiveCostPolicy) with no CLI surface yet — there is nosquad config costPolicysubcommand. The most direct exercise path is the vitest suite:Expected:
Tests 44 passed (44). The suite covers: AC5 implicit downgrade, AC6 explicit override warned-and-allowed, AC7 chain pruning to ceiling, AC8 fail-closed (no compliant model), AC9 empty-pruned-chain graceful degradation, AC10 out-of-catalog passthrough, AC11 end-to-end lifecycle wiring.For a runnable SDK demonstration (after building), write and run this temp file:
Expected output:
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com