mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 05:23:19 +00:00
fix(models): name dot-form Claude minors and future GPT ids consistently (#1531)
* fix(models): name dot-form Claude minors and future GPT ids consistently (#1530) One model id resolved to different display names per provider, and new models showed truncated names until a hand-maintained entry landed: - deriveClaudeShortName now accepts the dot-form minor some doors write on disk (GitHub Copilot's session store: claude-opus-4.8), so the name keeps its minor instead of truncating to "Opus 4". - Unknown dotted-version GPT ids derive their display name (gpt-5.7-terra -> "GPT-5.7 Terra") the way unreleased Claude versions already do, so a new release needs no SHORT_NAMES entry. Legacy bare-major / date-packaged ids (gpt-9, gpt-4-1106-preview) stay raw, and curated labels keep winning. - Pi/OMP/Copilot local display tables now match keys on segment boundaries via modelKeyMatches, so a bare `gpt-5` key can no longer capture `gpt-5.5` / `gpt-5.6-luna`; unmatched ids fall through to the global resolver, making one id show one name on every provider. - The JetBrains token scan rejects a trailing dot, so `gpt-5` / `claude-opus-4` cannot match inside `gpt-5.6-luna` / `claude-opus-4.8`. Fixes #1530 * fix(copilot): list the current JetBrains model ids in the token scan (#1530) Review of #1531: the tighter #1530 boundary is right, but with no token entries the newer ids fell through to copilot-anthropic-auto and priced as Sonnet 4.5. Add the current dotted ids, longest first per the list's order, so "model":"gpt-5.5", gpt-5.6-luna and claude-opus-4.8 resolve to their own priced, correctly named rows; unknown future versions (a dot followed by a digit) still return no token.
This commit is contained in:
parent
b6a977dd7c
commit
0259392a20
7 changed files with 225 additions and 33 deletions
|
|
@ -1423,6 +1423,10 @@ const SORTED_SHORT_NAMES: [string, string][] = Object.entries(SHORT_NAMES)
|
|||
// Anthropic's id scheme is `claude-<family>-<major>[-<minor>]`, so every new
|
||||
// version is derivable — no hand-maintained entry per release. (Legacy 3.x ids
|
||||
// put the family last, e.g. `claude-3-5-sonnet`, and stay in SHORT_NAMES.)
|
||||
// Some doors write the minor with a dot instead of a dash (GitHub Copilot's
|
||||
// session store: `claude-opus-4.8`), so both spellings derive the same name
|
||||
// (#1530); the dotted aliases in BUILTIN_ALIASES below predate this and stay
|
||||
// only because pricing also resolves through them.
|
||||
const CLAUDE_FAMILY: Record<string, string> = {
|
||||
opus: 'Opus',
|
||||
sonnet: 'Sonnet',
|
||||
|
|
@ -1431,19 +1435,50 @@ const CLAUDE_FAMILY: Record<string, string> = {
|
|||
mythos: 'Mythos',
|
||||
}
|
||||
function deriveClaudeShortName(canonical: string): string | undefined {
|
||||
const m = canonical.match(/^claude-(opus|sonnet|haiku|fable|mythos)-(\d+)(?:-(\d+))?/)
|
||||
const m = canonical.match(/^claude-(opus|sonnet|haiku|fable|mythos)-(\d+)(?:[-.](\d+))?/)
|
||||
if (!m) return undefined
|
||||
const [, family, major, minor] = m
|
||||
return `${CLAUDE_FAMILY[family]} ${major}${minor ? `.${minor}` : ''}`
|
||||
}
|
||||
|
||||
// OpenAI's id scheme is `gpt-<version>[-<variant>]` with a numeric version and
|
||||
// lowercase variant segments, so it derives the same way Anthropic's does — a
|
||||
// new release needs no hand-maintained entry (#1530). Consulted only after
|
||||
// SHORT_NAMES misses, so curated labels (GPT-5 Pro, GPT-5.1 Codex Mini, the
|
||||
// individually listed GPT-5.6 variants) keep winning. Guarded to the modern
|
||||
// dotted-version scheme (`gpt-5.7-terra`): bare-major ids are the legacy
|
||||
// date-packaged shape (`gpt-4-1106-preview`) and stay raw, and purely numeric
|
||||
// segments (1106, 0125, 20261105) are packaging, not the model's name.
|
||||
const GPT_DERIVED_ID = /^gpt-(\d+\.\d+(?:\.\d+)*)(?:-([a-z0-9][a-z0-9-]*))?$/
|
||||
function deriveGptShortName(id: string): string | undefined {
|
||||
const m = id.match(GPT_DERIVED_ID)
|
||||
if (!m) return undefined
|
||||
const [, version, variant] = m
|
||||
const segments = (variant ?? '')
|
||||
.split('-')
|
||||
.filter(seg => seg !== '' && !/^\d+$/.test(seg))
|
||||
.map(seg => seg.charAt(0).toUpperCase() + seg.slice(1))
|
||||
return ['GPT-' + version, ...segments].join(' ')
|
||||
}
|
||||
|
||||
function lookupShortName(id: string): string | undefined {
|
||||
const claude = deriveClaudeShortName(id)
|
||||
if (claude) return claude
|
||||
for (const [key, name] of SORTED_SHORT_NAMES) {
|
||||
if (id === key || id.startsWith(key + '-')) return name
|
||||
}
|
||||
return undefined
|
||||
return deriveGptShortName(id)
|
||||
}
|
||||
|
||||
/// Segment-boundary key match for provider display tables: `key` must be the
|
||||
/// whole id or a whole dash-segment of it, never mid-number or mid-version —
|
||||
/// so `gpt-5` cannot capture `gpt-5.5` or `gpt-5.6-luna`, exactly as the
|
||||
/// global table's `id === key || id.startsWith(key + '-')` cannot (#1530).
|
||||
/// Providers match their local tables with this so one model id resolves to
|
||||
/// one display name on every provider.
|
||||
export function modelKeyMatches(model: string, key: string): boolean {
|
||||
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
return new RegExp(`(?<![\\w.-])${escaped}(?=$|-)`).test(model)
|
||||
}
|
||||
|
||||
// Public API stays unary so Array.map/forEach cannot feed index as cycle state.
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ import { join, basename, dirname, posix, win32 } from 'path'
|
|||
import { existsSync } from 'fs'
|
||||
import { createHash } from 'crypto'
|
||||
import { readSessionFile } from '../fs-utils.js'
|
||||
import { calculateCost } from '../models.js'
|
||||
import { calculateCost, modelKeyMatches } from '../models.js'
|
||||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import { estimateTokens } from '../context-tree.js'
|
||||
import type {
|
||||
|
|
@ -1861,11 +1861,21 @@ function createChatSessionParser(
|
|||
// Known JetBrains Copilot model tokens, longest-first so we match the most
|
||||
// specific name (e.g. "gpt-4.1-mini" before "gpt-4.1").
|
||||
const JETBRAINS_MODEL_TOKENS = [
|
||||
'claude-opus-4.8',
|
||||
'claude-opus-4.6',
|
||||
'claude-opus-4.5',
|
||||
'claude-opus-4.1',
|
||||
'claude-opus-4',
|
||||
'claude-sonnet-4.6',
|
||||
'claude-sonnet-4.5',
|
||||
'claude-sonnet-4',
|
||||
'claude-haiku-4.5',
|
||||
'gpt-5.6-luna',
|
||||
'gpt-5.6-terra',
|
||||
'gpt-5.6',
|
||||
'gpt-5.5',
|
||||
'gpt-5.4-mini',
|
||||
'gpt-5.4',
|
||||
'gpt-5.3-codex',
|
||||
'gpt-5.3',
|
||||
'gpt-5.2',
|
||||
|
|
@ -1897,11 +1907,15 @@ function normalizeJetBrainsModelName(raw: string): string {
|
|||
}
|
||||
|
||||
/** Match a known model token at an alnum boundary anywhere in a string. */
|
||||
function findJetBrainsModelToken(s: string): string {
|
||||
export function findJetBrainsModelToken(s: string): string {
|
||||
for (const token of JETBRAINS_MODEL_TOKENS) {
|
||||
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
// "o3" etc. must not match inside words like "iso3166".
|
||||
if (new RegExp(`(?<![A-Za-z0-9])${escaped}(?![A-Za-z0-9])`).test(s)) {
|
||||
// "o3" etc. must not match inside words like "iso3166". A dot followed by
|
||||
// a digit is a version continuation and is rejected too: `gpt-5` must not
|
||||
// capture `gpt-5.6-luna`, nor `claude-opus-4` capture `claude-opus-4.8` —
|
||||
// that is a different version (#1530). A bare trailing dot (end of
|
||||
// sentence, file extension) is not a version and stays a boundary.
|
||||
if (new RegExp(`(?<![A-Za-z0-9])${escaped}(?![A-Za-z0-9]|\\.\\d)`).test(s)) {
|
||||
return normalizeJetBrainsModelName(token)
|
||||
}
|
||||
}
|
||||
|
|
@ -3689,9 +3703,13 @@ export function createCopilotProvider(
|
|||
return [...roots.values()]
|
||||
},
|
||||
|
||||
// Segment-boundary match (#1530): `includes` let a bare `gpt-5` key
|
||||
// capture `gpt-5.5` and `gpt-5.6-luna`. Unknown ids fall through to the
|
||||
// raw id, and the report layer's fallbackRawModelDisplayName then resolves
|
||||
// them with the same global getShortModelName every other provider uses.
|
||||
modelDisplayName(model: string): string {
|
||||
for (const [key, display] of modelDisplayEntries) {
|
||||
if (model.includes(key)) return display
|
||||
if (modelKeyMatches(model, key)) return display
|
||||
}
|
||||
return model
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,15 +9,6 @@ import { extractBashCommands } from '../bash-utils.js'
|
|||
import { normalizeContentBlocks } from '../content-utils.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
const modelDisplayNames: Record<string, string> = {
|
||||
'gpt-5.4': 'GPT-5.4',
|
||||
'gpt-5.4-mini': 'GPT-5.4 Mini',
|
||||
'gpt-5.5': 'GPT-5.5',
|
||||
'gpt-5': 'GPT-5',
|
||||
'gpt-4o': 'GPT-4o',
|
||||
'gpt-4o-mini': 'GPT-4o Mini',
|
||||
}
|
||||
|
||||
const toolNameMap: Record<string, string> = {
|
||||
bash: 'Bash',
|
||||
read: 'Read',
|
||||
|
|
@ -33,9 +24,6 @@ const toolNameMap: Record<string, string> = {
|
|||
patch: 'Patch',
|
||||
}
|
||||
|
||||
// Pre-sorted by key length descending so longer/more-specific keys match first
|
||||
const modelDisplayEntries = Object.entries(modelDisplayNames).sort((a, b) => b[0].length - a[0].length)
|
||||
|
||||
// Pi/OMP have no dedicated skill tool the way Claude Code does. A native skill
|
||||
// load is emitted as an ordinary `read` tool call whose path points at the
|
||||
// skill's `SKILL.md` (Pi resolves skills from many roots: ~/.pi/agent/skills,
|
||||
|
|
@ -440,10 +428,13 @@ export function createPiProvider(sessionsDir?: string): Provider {
|
|||
},
|
||||
displayName: 'Pi',
|
||||
|
||||
// No local display table (#1530): every entry one used to carry exists in
|
||||
// the global SHORT_NAMES with the same label, and a local list that lags
|
||||
// behind it re-creates the #1530 inconsistency (a bare `gpt-5` key
|
||||
// swallowing `gpt-5-mini` into "GPT-5"). Echoing the raw id lets the
|
||||
// report layer's fallbackRawModelDisplayName resolve every id with the
|
||||
// one global getShortModelName all providers share.
|
||||
modelDisplayName(model: string): string {
|
||||
for (const [key, name] of modelDisplayEntries) {
|
||||
if (model.startsWith(key)) return name
|
||||
}
|
||||
return model
|
||||
},
|
||||
|
||||
|
|
@ -475,9 +466,6 @@ export function createOmpProvider(sessionsDir?: string): Provider {
|
|||
displayName: 'OMP',
|
||||
|
||||
modelDisplayName(model: string): string {
|
||||
for (const [key, name] of modelDisplayEntries) {
|
||||
if (model.startsWith(key)) return name
|
||||
}
|
||||
return model
|
||||
},
|
||||
|
||||
|
|
|
|||
53
tests/model-display-consistency-1530.test.ts
Normal file
53
tests/model-display-consistency-1530.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { fallbackRawModelDisplayName } from '../src/models.js'
|
||||
import { pi, createOmpProvider } from '../src/providers/pi.js'
|
||||
import { copilot } from '../src/providers/copilot.js'
|
||||
|
||||
// The exact composition models-report/audit-report use: the provider's local
|
||||
// table first, then the global resolver when the provider echoed the raw id.
|
||||
// One model id must resolve to one display name on every provider (#1530).
|
||||
function displayNameOf(provider: { modelDisplayName?: (m: string) => string }, model: string): string {
|
||||
return fallbackRawModelDisplayName(provider.modelDisplayName!(model), model)
|
||||
}
|
||||
|
||||
describe('model display name consistency across providers (#1530)', () => {
|
||||
const omp = createOmpProvider()
|
||||
const providers = [
|
||||
{ name: 'pi', modelDisplayName: pi.modelDisplayName },
|
||||
{ name: 'omp', modelDisplayName: omp.modelDisplayName },
|
||||
{ name: 'copilot', modelDisplayName: copilot.modelDisplayName },
|
||||
]
|
||||
|
||||
// The rows from the issue: copilot and pi showed "GPT-5" / "Opus 4" because
|
||||
// a bare `gpt-5` key prefix-matched `gpt-5.5` / `gpt-5.6-luna`, and the
|
||||
// dot-form Claude minor was dropped by the dash-only derivation.
|
||||
it('shows GPT-5.5 and the GPT-5.6 variants on every provider', () => {
|
||||
for (const p of providers) {
|
||||
expect(displayNameOf(p, 'gpt-5.5')).toBe('GPT-5.5')
|
||||
expect(displayNameOf(p, 'gpt-5.6-terra')).toBe('GPT-5.6 Terra')
|
||||
expect(displayNameOf(p, 'gpt-5.6-luna')).toBe('GPT-5.6 Luna')
|
||||
expect(displayNameOf(p, 'gpt-5.5')).not.toBe('GPT-5')
|
||||
}
|
||||
})
|
||||
|
||||
it('shows the Claude minor for dot-form ids on every provider', () => {
|
||||
for (const p of providers) {
|
||||
expect(displayNameOf(p, 'claude-opus-4.8')).toBe('Opus 4.8')
|
||||
expect(displayNameOf(p, 'claude-sonnet-4.5')).toBe('Sonnet 4.5')
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves one id to the same name on every provider', () => {
|
||||
const ids = ['gpt-5.5', 'gpt-5.6-terra', 'gpt-5.6-luna', 'claude-opus-4.8', 'gpt-5.4-mini', 'gpt-5-mini', 'gpt-5', 'gpt-4o']
|
||||
for (const id of ids) {
|
||||
const names = new Set(providers.map(p => displayNameOf(p, id)))
|
||||
expect([...names], id).toHaveLength(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the provider-specific auto-model overrides', () => {
|
||||
expect(displayNameOf(copilot, 'copilot-openai-auto')).toBe('Copilot (OpenAI auto)')
|
||||
expect(displayNameOf(copilot, 'copilot-anthropic-auto')).toBe('Copilot (Anthropic auto)')
|
||||
})
|
||||
})
|
||||
|
|
@ -24,6 +24,7 @@ import {
|
|||
getFlatRateModelsConfigHash,
|
||||
parseLiteLLMEntry,
|
||||
unpricedModelHint,
|
||||
modelKeyMatches,
|
||||
} from '../src/models.js'
|
||||
import { getDailyCacheConfigHash } from '../src/usage-aggregator.js'
|
||||
import snapshotData from '../src/data/litellm-snapshot.json' with { type: 'json' }
|
||||
|
|
@ -354,9 +355,42 @@ describe('getShortModelName', () => {
|
|||
expect(getShortModelName('gpt-5.6-sol')).toBe('GPT-5.6 Sol')
|
||||
expect(getShortModelName('gpt-5.6-terra')).toBe('GPT-5.6 Terra')
|
||||
expect(getShortModelName('gpt-5.6-luna')).toBe('GPT-5.6 Luna')
|
||||
// No bare `gpt-5.6` entry exists, so an unlisted future variant must still
|
||||
// fall through to its raw id rather than borrow a sibling's label.
|
||||
expect(getShortModelName('gpt-5.6-unlisted')).toBe('gpt-5.6-unlisted')
|
||||
// No bare `gpt-5.6` entry exists, so an unlisted variant of this version
|
||||
// must not borrow a sibling's curated label. Since #1530 it derives its own
|
||||
// label from the id instead of surfacing the raw slug. (Versions that DO
|
||||
// have a bare entry, like gpt-5.5, still fold suffixed ids by the prefix
|
||||
// rule — unchanged here.)
|
||||
expect(getShortModelName('gpt-5.6-unlisted')).toBe('GPT-5.6 Unlisted')
|
||||
})
|
||||
|
||||
// Regression for #1530: some doors write the Claude minor with a dot
|
||||
// (GitHub Copilot's session store: claude-opus-4.8). The derivation must
|
||||
// accept both spellings, or the name silently loses its minor ("Opus 4").
|
||||
it('derives dot-form Claude minors the same as dash-form (#1530)', () => {
|
||||
expect(getShortModelName('claude-opus-4.8')).toBe('Opus 4.8')
|
||||
expect(getShortModelName('claude-opus-4-8')).toBe('Opus 4.8')
|
||||
expect(getShortModelName('claude-sonnet-4.9')).toBe('Sonnet 4.9')
|
||||
expect(getShortModelName('claude-opus-4.8-20300101')).toBe('Opus 4.8')
|
||||
})
|
||||
|
||||
// Regression for #1530: an unknown future GPT version derives its name from
|
||||
// the id, the way unreleased Claude versions already do — no hand-maintained
|
||||
// entry per release. Curated entries still win, and the legacy bare-major /
|
||||
// date-packaged shapes stay raw.
|
||||
it('derives unknown GPT versions from their ids instead of surfacing raw (#1530)', () => {
|
||||
expect(getShortModelName('gpt-5.7-terra')).toBe('GPT-5.7 Terra')
|
||||
expect(getShortModelName('gpt-5.7-codex-spark')).toBe('GPT-5.7 Codex Spark')
|
||||
expect(getShortModelName('gpt-5.7')).toBe('GPT-5.7')
|
||||
// Numeric segments are packaging, not part of the name.
|
||||
expect(getShortModelName('gpt-5.7-20261105')).toBe('GPT-5.7')
|
||||
// Legacy shapes stay raw: bare-major ids and date-versioned packaging.
|
||||
expect(getShortModelName('gpt-9')).toBe('gpt-9')
|
||||
expect(getShortModelName('gpt-4-1106-preview')).toBe('gpt-4-1106-preview')
|
||||
// Curated labels keep winning over the derivation.
|
||||
expect(getShortModelName('gpt-5.5')).toBe('GPT-5.5')
|
||||
expect(getShortModelName('gpt-5.1-codex-mini')).toBe('GPT-5.1 Codex Mini')
|
||||
expect(getShortModelName('gpt-5-mini')).toBe('GPT-5 Mini')
|
||||
expect(getShortModelName('gpt-4o')).toBe('GPT-4o')
|
||||
})
|
||||
|
||||
it('names grok-4.5 without disturbing the Grok Build harness label', () => {
|
||||
|
|
@ -390,6 +424,26 @@ describe('getShortModelName', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('modelKeyMatches', () => {
|
||||
// The primitive provider display tables match with (#1530): a key must be
|
||||
// the whole id or a whole dash-segment, never mid-version — so a bare
|
||||
// `gpt-5` key cannot capture `gpt-5.5` or `gpt-5.6-luna`.
|
||||
it('matches the exact id and dash-suffixed ids', () => {
|
||||
expect(modelKeyMatches('gpt-5', 'gpt-5')).toBe(true)
|
||||
expect(modelKeyMatches('gpt-5-mini', 'gpt-5')).toBe(true)
|
||||
expect(modelKeyMatches('gpt-4.1-2025-04-14', 'gpt-4.1')).toBe(true)
|
||||
expect(modelKeyMatches('openai/gpt-5', 'gpt-5')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects mid-version and mid-word matches', () => {
|
||||
expect(modelKeyMatches('gpt-5.5', 'gpt-5')).toBe(false)
|
||||
expect(modelKeyMatches('gpt-5.6-luna', 'gpt-5')).toBe(false)
|
||||
expect(modelKeyMatches('gpt-5.4.1', 'gpt-5.4')).toBe(false)
|
||||
expect(modelKeyMatches('xgpt-5', 'gpt-5')).toBe(false)
|
||||
expect(modelKeyMatches('claude-opus-4.8', 'claude-opus-4')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('claude-fable-5 pricing + name', () => {
|
||||
it('prices at $10/M input, $50/M output via models.dev/OpenRouter gap-fill', () => {
|
||||
expect(calculateCost('claude-fable-5', 1_000_000, 0, 0, 0, 0)).toBeCloseTo(10, 6)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { createRequire } from 'node:module'
|
|||
import { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { copilot, createCopilotProvider, getVSCodeGlobalStorageDirs, getVSCodeWorkspaceStorageDirs, ChatCompletionContentPartKind } from '../../src/providers/copilot.js'
|
||||
import { ChatCompletionContentPartKind, copilot, createCopilotProvider, findJetBrainsModelToken, getVSCodeGlobalStorageDirs, getVSCodeWorkspaceStorageDirs } from '../../src/providers/copilot.js'
|
||||
import { isSqliteAvailable, isSqliteBusyError } from '../../src/sqlite.js'
|
||||
import { calculateCost } from '../../src/models.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
|
|
@ -4319,3 +4319,40 @@ describe('copilot deduplication key prefixes (durable-union contract)', () => {
|
|||
expect(/:shutdown-residual:[^:]+:\d{13}$/.test(residual)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('copilot provider - JetBrains model token scan', () => {
|
||||
// #1530: a token must not capture a newer version — `gpt-5` is not
|
||||
// `gpt-5.7-terra`, nor `claude-opus-4` `claude-opus-4.9`. A dot only counts
|
||||
// as a version continuation when a digit follows it; an end-of-sentence
|
||||
// period or a file extension stays a token boundary.
|
||||
it('does not match a token inside a newer dotted version', () => {
|
||||
expect(findJetBrainsModelToken('running on gpt-5.7-terra for coding')).toBe('')
|
||||
expect(findJetBrainsModelToken('switched to claude-opus-4.9 today')).toBe('')
|
||||
expect(findJetBrainsModelToken('gpt-5.4.1 beta')).toBe('')
|
||||
})
|
||||
|
||||
// The ids JetBrains writes today must match their own token, not fall
|
||||
// through to the anthropic-auto bucket (#1531 review).
|
||||
it('recognises the current dotted ids, longest first', () => {
|
||||
expect(findJetBrainsModelToken('"model":"gpt-5.5"')).toBe('gpt-5.5')
|
||||
expect(findJetBrainsModelToken('running on gpt-5.6-luna for coding')).toBe('gpt-5.6-luna')
|
||||
expect(findJetBrainsModelToken('running on gpt-5.6-terra for coding')).toBe('gpt-5.6-terra')
|
||||
expect(findJetBrainsModelToken('"model":"gpt-5.6"')).toBe('gpt-5.6')
|
||||
expect(findJetBrainsModelToken('gpt-5.4-mini beta')).toBe('gpt-5.4-mini')
|
||||
expect(findJetBrainsModelToken('switched to claude-opus-4.8 today')).toBe('claude-opus-4-8')
|
||||
expect(findJetBrainsModelToken('switched to claude-opus-4.6 today')).toBe('claude-opus-4-6')
|
||||
expect(findJetBrainsModelToken('switched to claude-sonnet-4.6 today')).toBe('claude-sonnet-4-6')
|
||||
expect(findJetBrainsModelToken('switched to claude-haiku-4.5 today')).toBe('claude-haiku-4-5')
|
||||
})
|
||||
|
||||
it('still matches tokens followed by sentence punctuation or file extensions', () => {
|
||||
expect(findJetBrainsModelToken('switched to gpt-5. thanks')).toBe('gpt-5')
|
||||
expect(findJetBrainsModelToken('see configs/gpt-5.json for details')).toBe('gpt-5')
|
||||
expect(findJetBrainsModelToken('running on claude-opus-4.5.')).toBe('claude-opus-4-5')
|
||||
expect(findJetBrainsModelToken('"model":"gpt-5"')).toBe('gpt-5')
|
||||
})
|
||||
|
||||
it('does not match inside words', () => {
|
||||
expect(findJetBrainsModelToken('iso3166 codes')).toBe('')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from 'path'
|
|||
import { tmpdir } from 'os'
|
||||
|
||||
import { createPiProvider, createOmpProvider } from '../../src/providers/pi.js'
|
||||
import { fallbackRawModelDisplayName } from '../../src/models.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
import { classifyTurn } from '../../src/classifier.js'
|
||||
import type { ParsedApiCall, ParsedTurn } from '../../src/types.js'
|
||||
|
|
@ -640,6 +641,11 @@ describe('pi provider - JSONL parsing', () => {
|
|||
|
||||
describe('pi provider - display names', () => {
|
||||
const provider = createPiProvider('/tmp')
|
||||
// The composition models-report/audit-report actually use: the provider
|
||||
// hook first, then the global resolver when the hook echoed the raw id.
|
||||
// Pi carries no local table anymore (#1530), so every name comes from the
|
||||
// one global getShortModelName all providers share.
|
||||
const display = (m: string) => fallbackRawModelDisplayName(provider.modelDisplayName(m), m)
|
||||
|
||||
it('has correct name and displayName', () => {
|
||||
expect(provider.name).toBe('pi')
|
||||
|
|
@ -647,13 +653,14 @@ describe('pi provider - display names', () => {
|
|||
})
|
||||
|
||||
it('maps known models to readable names', () => {
|
||||
expect(provider.modelDisplayName('gpt-5.4')).toBe('GPT-5.4')
|
||||
expect(provider.modelDisplayName('gpt-5.4-mini')).toBe('GPT-5.4 Mini')
|
||||
expect(provider.modelDisplayName('gpt-5')).toBe('GPT-5')
|
||||
expect(display('gpt-5.4')).toBe('GPT-5.4')
|
||||
expect(display('gpt-5.4-mini')).toBe('GPT-5.4 Mini')
|
||||
expect(display('gpt-5')).toBe('GPT-5')
|
||||
expect(display('gpt-5-mini')).toBe('GPT-5 Mini')
|
||||
})
|
||||
|
||||
it('returns raw name for unknown models', () => {
|
||||
expect(provider.modelDisplayName('some-future-model')).toBe('some-future-model')
|
||||
expect(display('some-future-model')).toBe('some-future-model')
|
||||
})
|
||||
|
||||
it('normalizes tool names to capitalized form', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue