Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/probe-patch-sites.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const EXPECTED_PATCH_SITES = Object.freeze([
'PATCH 6: alias resolver switch',
'PATCH 5: model picker options',
'PATCH 4: Agent tool model description',
'PATCH 11: model self-identity',
'PATCH 7: per-model context window',
'PATCH 8a: effort capability',
'PATCH 8b: xhigh effort capability',
Expand Down
4 changes: 4 additions & 0 deletions src/built-in-patch-proofs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ export function captureBuiltInPatchProofs(
'PATCH 9: default effort',
/\/\*ccpatch:default-effort\*\/var _cce=Object\.assign\(Object\.create\(null\),\{[^{}]*\}\)\[String\([\w$]+\|\|""\)\.trim\(\)\.toLowerCase\(\)\];if\(_cce!==void 0\)return _cce;/,
);
addPattern(
'PATCH 11: model self-identity',
/\/\*ccpatch:identity\*\/\(\{[^{}]*\}\)\[String\([\w$]+\|\|""\)\.trim\(\)\.toLowerCase\(\)\]/,
);
addPattern(
'PATCH 10: child network environment',
/\/\*ccpatch:child-network-env\*\/let _clodexChildEnv=process\.env,[\s\S]*?catch\(_clodexError\)\{\}\}/,
Expand Down
61 changes: 60 additions & 1 deletion src/patch-transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,13 @@ import {
* bumped because the rule above says a changed anchor is bumped — an install
* patched by an older clodex is not wrong, but it was produced by a transform set
* whose anchors are narrower, so it re-reads as stale rather than current.
*
* 13 — PATCH 11 added: a custom model's identity line in the system prompt now
* carries its real label and upstream model id instead of only its short alias.
* An install without it still routes correctly; it re-reads as stale so the next
* patch run gives routed models their names.
*/
export const PATCH_TRANSFORMS_VERSION = 12;
export const PATCH_TRANSFORMS_VERSION = 13;

export interface PatchScriptModelEntry {
alias?: string;
Expand Down Expand Up @@ -519,6 +524,60 @@ export function applyClodexPatches(source: string, config: PatchScriptModelConfi
);
}

// ---------------------------------------------------------------------------
// PATCH 11 — the model's own identity line in the system prompt.
//
// Claude Code tells every model what it is in the env_info section: "You are
// powered by the model named <marketing name>. The exact model ID is <id>."
// when its catalog knows the id, and only "You are powered by the model <id>."
// when it does not. A custom model's id is its short alias, so without this a
// routed model is told it is "luna" or "flash" and nothing else: asked what it
// is, it answers with the alias, and it cannot tie itself to the "Additional
// custom models" listing PATCH 4 writes (measured on 2.1.268: a Flash session
// quoted that listing and still called itself "flash").
//
// We extend the marketing-name lookup with a baked table, so a configured model
// renders the first sentence with its real label and upstream model id. Only
// the object that feeds that sentence is touched; every other caller of the
// marketing-name function keeps its native answer.
//
// Anchor: the object literal `{modelId:X,marketingName:F(X)??null,
// knowledgeCutoff:G(X)}`, every identifier wildcarded and the parameter tied
// together by back-reference. Best-effort (cosmetic), like PATCH 4.
// ---------------------------------------------------------------------------
{
const IDENTITY_MARKER = '/*ccpatch:identity*/';
const NAME_BY_KEY: Record<string, string> = Object.create(null);
for (const [id, value] of Object.entries(MODEL_CONFIG)) {
const spec: PatchScriptModelEntry = value && typeof value === 'object' ? value : { alias: value as unknown as string };
const segments = String(id).split(':');
const upstream = (segments.length >= 3 ? segments.slice(2).join(':') : String(id))
.replace(/\[1m\]$/i, '');
const label = spec.display ? String(spec.display) : upstream;
const name = label + '; upstream model id ' + upstream + ', served through Clodex';
if (spec.alias !== undefined) NAME_BY_KEY[String(spec.alias).trim().toLowerCase()] = name;
NAME_BY_KEY[String(id).trim().toLowerCase()] = name;
}
const lookupFor = (modelParam: string) =>
IDENTITY_MARKER + '(' + JSON.stringify(NAME_BY_KEY) + ')[String(' + modelParam + '||"").trim().toLowerCase()]';

if (js.includes(IDENTITY_MARKER)) {
applyOnce(
'PATCH 11: model self-identity (refresh)',
/\/\*ccpatch:identity\*\/\(\{[^{}]*\}\)\[String\(([\w$]+)\|\|""\)\.trim\(\)\.toLowerCase\(\)\]/,
(_m, modelParam) => lookupFor(modelParam!),
{ required: false, noopIsSkip: true }
);
} else {
applyOnce(
'PATCH 11: model self-identity',
/(\{modelId:([\w$]+),marketingName:[\w$]+\(\2\))(\?\?null,knowledgeCutoff:[\w$]+\(\2\)\})/,
(_m, head, modelParam, tail) => head! + '??' + lookupFor(modelParam!) + tail!,
{ required: false }
);
}
}

// ---------------------------------------------------------------------------
// PATCH 7 — per-model context window.
//
Expand Down
11 changes: 11 additions & 0 deletions tests/fixtures/claude-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,24 @@ export function contextResolver(modelParam: string, windowParam: string): string
+ `if(EHi(${modelParam},${windowParam}))return Dve;return $Ac(${modelParam},${windowParam})}`;
}

/**
* The object feeding Claude Code's env_info identity sentence ("You are powered by the model
* named <marketing name>. The exact model ID is <id>."), as 2.1.268 spells it. PATCH 11 keys on
* it. The marketing-name lookup answers only for native models, so a custom model is otherwise
* told nothing but its short alias.
*/
export const IDENTITY_SITE =
'function Ot(e){return{modelId:e,marketingName:Bu(e)??null,knowledgeCutoff:Lc(e)}}'
+ 'function Bu(e){return e==="claude-opus-5"?"Opus 5":void 0}function Lc(e){return null}';

export const CLAUDE_CORE_FIXTURE = [
ENUM_AND_DESCRIPTION,
'var KNOWN=["sonnet","opus","haiku","fable","opusplan"];',
'function rz(x){switch(x){case"best":{return "opus"}default:return null}}',
'function opts(e,t,r){let n=cur(),o=(n==="opus"||n==="sonnet")&&n!==r?[n,r]:[r];for(let i of o)Dlh(e,i,t);return e}',
CONTEXT_RESOLVER,
'function cwdOf(){let p=process.env.PWD;return p}',
IDENTITY_SITE,
'function childEnv(){let e=extra(),t=Object.keys(e).length>0,n=Object.keys(e).length>0,s=flag(process.env.CLAUDE_CODE_REMOTE)?remote():{};let o=[process.env.CLAUDE_CODE_OAUTH_TOKEN,process.env.CLAUDE_CODE_SUBSCRIPTION_TYPE,process.env.CLAUDE_BG_PTY_AUTH,"OTEL_",process.env.CLAUDE_CODE_OTEL_DIAG_STDERR],u=["CLAUDE_CODE_OAUTH_TOKEN"];if(!t&&!n&&!o[0])return process.env;let v={...process.env,...e,...s};for(let k of u)delete v[k],delete v[`INPUT_${k}`];return v}function mcpAllow(){let e=process.env.CLAUDE_CODE_MCP_ALLOWLIST_ENV;return e}',
].join('\n');

Expand Down
63 changes: 57 additions & 6 deletions tests/patcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CLAUDE_SPLIT_MODULES,
CONTEXT_RESOLVER,
contextResolver,
IDENTITY_SITE,
} from './fixtures/claude-bundle.js';
import {
buildFakeElfClaude,
Expand Down Expand Up @@ -588,8 +589,8 @@ describe('PATCH_TRANSFORMS_VERSION', () => {
.join('\n');
const digest = createHash('sha256').update(source).digest('hex');
expect({ version: PATCH_TRANSFORMS_VERSION, digest }).toEqual({
version: 12,
digest: 'd9dff2594fc60dcae83fb34846c681ee75fb3b0d7f49e5c26cb1c3c415cbcc4c',
version: 13,
digest: '745929da01e2e7e67c1f7db53e76eb1078eceade571e36ca173f938accb22b04',
});
});
});
Expand Down Expand Up @@ -2722,6 +2723,53 @@ describe('patch script identity naming', () => {
+ 'clodex:openai:mystery = Mystery (OpenAI).');
});

const identityOf = (source: string, model: string): { modelId: string; marketingName: string | null } =>
new Function(source.split('\n').filter(line => line.includes('knowledgeCutoff:')).join('\n')
+ ';return Ot(' + JSON.stringify(model) + ')')();

it('PATCH 11 gives a routed model its real name and upstream id in the identity line', () => {
const result = applyClodexPatches(CLAUDE_FIXTURE, {
'clodex:openai-oauth:gpt-5.6-luna': { alias: 'luna', context: 272_000, display: 'GPT-5.6 Luna (OpenAI (ChatGPT))' },
'clodex:opencode-go:deepseek-v4.1-flash': { alias: 'flash', display: 'DeepSeek V4.1 Flash (OpenCode Go)' },
});
expect(result.results.find(r => r.name === 'PATCH 11: model self-identity')!.status).toBe('OK');
const want = 'GPT-5.6 Luna (OpenAI (ChatGPT)); upstream model id gpt-5.6-luna, served through Clodex';
// Reached by the alias Claude Code sends AND by the full clodex: id, case-insensitively.
expect(identityOf(result.content, 'luna').marketingName).toBe(want);
expect(identityOf(result.content, 'LUNA').marketingName).toBe(want);
expect(identityOf(result.content, 'clodex:openai-oauth:gpt-5.6-luna').marketingName).toBe(want);
expect(identityOf(result.content, 'flash').marketingName)
.toBe('DeepSeek V4.1 Flash (OpenCode Go); upstream model id deepseek-v4.1-flash, served through Clodex');
// Native models keep the native answer; unknown ids stay null.
expect(identityOf(result.content, 'claude-opus-5').marketingName).toBe('Opus 5');
expect(identityOf(result.content, 'some-other-model').marketingName).toBeNull();
});

it('PATCH 11 falls back to the upstream id when no display label is known, and refreshes in place', () => {
const first = applyClodexPatches(CLAUDE_FIXTURE, {
'clodex:openai-oauth:gpt-6-astra': { alias: 'astra' },
});
expect(identityOf(first.content, 'astra').marketingName)
.toBe('gpt-6-astra; upstream model id gpt-6-astra, served through Clodex');
// Re-patching an already-patched source replaces the table rather than stacking a second one.
const second = applyClodexPatches(first.content, {
'clodex:openai-oauth:gpt-6-astra': { alias: 'astra', display: 'GPT-6 Astra (OpenAI (ChatGPT))' },
});
expect(second.content.split('/*ccpatch:identity*/').length).toBe(2);
expect(identityOf(second.content, 'astra').marketingName)
.toBe('GPT-6 Astra (OpenAI (ChatGPT)); upstream model id gpt-6-astra, served through Clodex');
});

it('PATCH 11 is best-effort: a bundle without the identity site still patches', () => {
const withoutSite = CLAUDE_FIXTURE.replace(IDENTITY_SITE, 'function Ot(e){return e}');
expect(withoutSite).not.toBe(CLAUDE_FIXTURE);
const result = applyClodexPatches(withoutSite, {
'clodex:openai-oauth:gpt-5.6-luna': { alias: 'luna' },
});
expect(result.results.find(r => r.name === 'PATCH 11: model self-identity')!.status).not.toBe('OK');
expect(result.content).not.toContain('/*ccpatch:identity*/');
});

it('falls back to the old "Custom model (id)" description when no label is known', () => {
const out = runPatchScript({ 'clodex:openai-oauth:gpt-5.6-sol': { alias: 'sol', context: 272_000 } });
expect(out).toContain('{value:"sol",label:"Sol",description:"Custom model (clodex:openai-oauth:gpt-5.6-sol)"}');
Expand Down Expand Up @@ -2933,6 +2981,7 @@ describe('patch script identity naming', () => {
['PATCH 6: alias resolver switch', 'OK'],
['PATCH 5: model picker options', 'OK'],
['PATCH 4: Agent tool model description', 'OK'],
['PATCH 11: model self-identity', 'OK'],
['PATCH 7: per-model context window', 'OK'],
['PATCH 8a: effort capability', 'OK'],
['PATCH 8b: xhigh effort capability', 'OK'],
Expand All @@ -2947,8 +2996,9 @@ describe('patch script identity naming', () => {
['PATCH 6: alias resolver switch', 'SKIP'],
['PATCH 5: model picker options', 'SKIP'],
['PATCH 4: Agent tool model description', 'SKIP'],
// PATCH 7 re-runs through the in-place refresh path; an unchanged config
// rewrites the identical table, which reports as already patched.
// PATCH 7 and PATCH 11 re-run through their in-place refresh paths; an
// unchanged config rewrites the identical table, which reports as already patched.
['PATCH 11: model self-identity (refresh)', 'SKIP'],
['PATCH 7: per-model context window (refresh)', 'SKIP'],
['PATCH 8a: effort capability (refresh)', 'SKIP'],
['PATCH 8b: xhigh effort capability (refresh)', 'SKIP'],
Expand Down Expand Up @@ -3094,15 +3144,16 @@ describe('patch script identity naming', () => {

expect(patched.content).toContain('.enum(["sonnet","opus","haiku","fable","sol","clodex:openai:mystery"])');
expect(patched.content).toContain('/*ccpatch:ctx*/');
expect(patched.results.slice(0, 6).map(result => [result.name, result.status])).toEqual([
expect(patched.results.slice(0, 7).map(result => [result.name, result.status])).toEqual([
['PATCH 1: Agent tool model enum', 'OK'],
['PATCH 3: known-alias validator list', 'OK'],
['PATCH 6: alias resolver switch', 'OK'],
['PATCH 5: model picker options', 'OK'],
['PATCH 4: Agent tool model description', 'OK'],
['PATCH 11: model self-identity', 'OK'],
['PATCH 7: per-model context window', 'OK'],
]);
expect(patched.results.slice(6, -1)).toEqual([
expect(patched.results.slice(7, -1)).toEqual([
{ status: 'FAIL', name: 'PATCH 8a: effort capability', extra: 'anchor not found' },
{ status: 'FAIL', name: 'PATCH 8b: xhigh effort capability', extra: 'anchor not found' },
{ status: 'FAIL', name: 'PATCH 8c: max effort capability', extra: 'anchor not found' },
Expand Down