From 908bdd00adb09165ce6cb962354bcdbc74ed0d62 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 16:25:41 +0700 Subject: [PATCH 1/2] T27 OS: the profile chain walks z.ai free, then OpenRouter's :free ids, then the paid profiles, and a missing OpenRouter key skips its tier (Refs #11285) keys.t27: PROFILE_TIER / profile_rank put the chain in tier order (CT_ZAI_FREE, CT_OR_FREE, CT_PAID); next_profile walks by rank, so a pane whose z.ai keys all rest moves to OpenRouter when a key is stored and to the paid profile (or the give-up line) when it is not. The openrouter profile now names three free ids (public list 2026-10-20, not yet measured on a key: none is stored) at model steps 0..2 (MODELS_DEFAULT/SECOND/THIRD, MODEL_STEPS 3) through the same Anthropic-protocol variables as before; its paid ~anthropic tier names are gone. needs_key_request(provider, stored_count) and KEY_REQUEST_OPENROUTER are the predicate and the line for the KEYS tab (FR-003; the tab itself is not here). MAX_FAILOVERS 24 -> 48 for two multi-model free tiers. session_store.t27: model_ok accepts "/" and ":" so an OpenRouter id is not dropped from the --model flag (quoted in single quotes, so plain bytes). pty_logic.t27: pane-level tests of the whole walk (z.ai keys and models, then OpenRouter's three ids, then the paid profile, then the give-up), of the skip with no OpenRouter key, and of the command line and environment of an OpenRouter pane. Co-Authored-By: Claude Sonnet 5.5 --- specs/t27os/keys.t27 | 400 ++++++++++++++++++++++++++++------ specs/t27os/pty_logic.t27 | 136 ++++++++++++ specs/t27os/session_store.t27 | 15 +- 3 files changed, 482 insertions(+), 69 deletions(-) diff --git a/specs/t27os/keys.t27 b/specs/t27os/keys.t27 index 7affbaf903..7ff15815bd 100644 --- a/specs/t27os/keys.t27 +++ b/specs/t27os/keys.t27 @@ -624,7 +624,9 @@ test the_keys_link_sits_left_of_the_doctor_link { // the brace module syntax and cannot be `use`d from here, so the order is restated once, below, and // nowhere else. Kie and nemotron are not Anthropic-protocol endpoints; they join in stage 2 (the local // router, which translates through specs/provider/transform.t27). OpenRouter and Ollama's cloud endpoint -// do document the Anthropic protocol (see PROFILE_PROVIDER below), so they are profiles now. +// do document the Anthropic protocol (see PROFILE_PROVIDER below), so they are profiles now. The profile +// numbers below index every table; the order the chain walks them in is the tier order further down +// (PROFILE_TIER, profile_rank): z.ai free, then OpenRouter's `:free` ids, then the paid profiles. pub const PR_ZAI : u32 = 0; pub const PR_ANTHROPIC : u32 = 1; pub const PR_OPENROUTER : u32 = 2; @@ -634,6 +636,24 @@ pub const PROFILE_COUNT : u32 = 4; pub const PROFILE_NAMES : str = "zai,anthropic-api,openrouter,ollama-cloud"; /// The provider whose Keychain key backs each profile. pub const PROFILE_PROVIDER : [4]u32 = [4, 0, 3, 5]; + +// ---- the tier order of the chain (gHashTag/t27#11285, FR-001) ---------------------------------------------- +// Owner, 2026-10-20: free z.ai flash by default, OpenRouter's free tokens as well, never run out of tokens, +// keys rotate on errors. The chain has three tiers, walked in this order, and a pane tries ALL the keys of a +// tier (rotate_pick_except: the failed key rests, the next one is taken, fo_action) and every model of the tier +// (fo_next_model) before it moves to the next tier: +// 0 CT_ZAI_FREE z.ai's free flash models (profile zai) +// 1 CT_OR_FREE OpenRouter's `:free` model ids through an OpenRouter key from the Keychain (profile +// openrouter). With no OpenRouter key stored the tier is skipped, never an error loop: +// next_profile offers a profile only when its provider holds a key (needs_key_request says so) +// 2 CT_PAID the paid profiles (anthropic-api, ollama-cloud), only after both free tiers rest +// PROFILE_NAMES stays the index order of every table; the chain order is the tier first and the index second. +pub const CT_ZAI_FREE : u32 = 0; +pub const CT_OR_FREE : u32 = 1; +pub const CT_PAID : u32 = 2; +pub const CT_COUNT : u32 = 3; +/// The tier of each profile, in PROFILE_NAMES order: zai, anthropic-api, openrouter, ollama-cloud. +pub const PROFILE_TIER : [4]u32 = [0, 2, 1, 2]; // A profile exists only for a provider that documents an Anthropic-protocol base URL on its own pages, read // on 2026-10-20 (the page is named at each row below). None is used until its provider's key is stored. // zai https://docs.z.ai/devpack/tool/claude @@ -674,17 +694,27 @@ pub const TIER_VARS : str = "ANTHROPIC_MODEL,ANTHROPIC_SMALL_FAST_MODEL,ANTHROPI /// A pane (new or restored) starts on the default; on 429, 1113, 1305 or 401/403 it rotates over every key of /// the provider (the failed key resting), then takes the next model of the tier (MODELS_SECOND) and rotates /// over the keys again, then leaves for the next profile of the chain (fo_action). -/// Fields: zai, anthropic-api, openrouter, ollama-cloud. OpenRouter's page gives the three tier names and no -/// default (so ANTHROPIC_MODEL stays unset there); Ollama's page names one cloud model, used for every tier. -pub const MODELS_DEFAULT : str = "glm-4.5-flash,,,glm-5.3-flash"; +/// OpenRouter's tier (CT_OR_FREE, gHashTag/t27#11285): its `:free` model ids, taken from the public model list +/// https://openrouter.ai/api/v1/models read on 2026-10-20 (17 ids end in `:free`; these three list "tools" and +/// "tool_choice" among their supported parameters and a context of 1,000,000 tokens). NOT YET MEASURED ON A KEY: +/// no OpenRouter key is stored in the Keychain (the one found in old env files answers 401), so the order is the +/// list's, not a measurement. Measure each id with one 1-token call the day a key is stored and reorder this +/// table as the z.ai one was. Every tier of the profile names the default free id (the tier columns below are +/// empty for it); the paid `~anthropic/...` names OpenRouter also serves are not used by the free tier. +/// Fields: zai, anthropic-api, openrouter, ollama-cloud. Ollama's page names one cloud model, used for every tier. +pub const MODELS_DEFAULT : str = "glm-4.5-flash,,minimax/minimax-m3.1-flash:free,glm-5.3-flash"; /// The second model of each profile's tier (empty = the profile has one model): what a pane moves to when /// every key of its provider failed on the default. The same fields as MODELS_DEFAULT. -pub const MODELS_SECOND : str = "glm-4.7-flash,,,"; -/// How many models one profile's tier map can hold: step 0 is MODELS_DEFAULT, step 1 is MODELS_SECOND. -pub const MODEL_STEPS : u32 = 2; -pub const MODELS_HAIKU : str = ",,~anthropic/claude-haiku-latest,"; -pub const MODELS_SONNET : str = ",,~anthropic/claude-sonnet-latest[1m],"; -pub const MODELS_OPUS : str = ",,~anthropic/claude-opus-latest[1m],"; +pub const MODELS_SECOND : str = "glm-4.7-flash,,nvidia/nemotron-3.5-super-vl:free,"; +/// The third model of each profile's tier (empty = the profile has fewer models). A profile that names a third +/// model names a second one too (profile_models counts up to the first empty step). +pub const MODELS_THIRD : str = ",,nvidia/nemotron-3.5-lightning:free,"; +/// How many models one profile's tier map can hold: step 0 is MODELS_DEFAULT, step 1 is MODELS_SECOND, step 2 +/// is MODELS_THIRD. +pub const MODEL_STEPS : u32 = 3; +pub const MODELS_HAIKU : str = ",,,"; +pub const MODELS_SONNET : str = ",,,"; +pub const MODELS_OPUS : str = ",,,"; /// The Anthropic-protocol base URL of each profile; empty = Claude Code's own (the direct API). pub const PROFILE_BASE_URLS : str = "https://api.z.ai/api/anthropic,,https://openrouter.ai/api,https://ollama.com"; /// The variable that carries the Keychain key: a bearer token for a gateway, the API key for Anthropic itself. @@ -742,20 +772,23 @@ pub fn tier_model(pr: u32, ti: u32, out: []u8) -> usize { return csv_field(MODELS_DEFAULT, pr, out); } -/// The model profile pr serves for tier ti at model step `step` (0 = MODELS_DEFAULT: tier_model; 1 = the -/// second model of the tier, MODELS_SECOND, which every tier of that profile names); 0 when there is none. +/// The model profile pr serves for tier ti at model step `step` (0 = MODELS_DEFAULT: tier_model; 1 and 2 = the +/// second and third model of the tier, MODELS_SECOND and MODELS_THIRD, which every tier of that profile +/// names); 0 when there is none. pub fn tier_model_step(pr: u32, ti: u32, step: u32, out: []u8) -> usize { if (step == 0) { return tier_model(pr, ti, out); } if (step >= MODEL_STEPS or pr >= PROFILE_COUNT or ti >= TIER_COUNT) { return 0; } - return csv_field(MODELS_SECOND, pr, out); + if (step == 1) { return csv_field(MODELS_SECOND, pr, out); } + return csv_field(MODELS_THIRD, pr, out); } -/// How many models profile pr's tier map holds: 1 or 2 (0 for no profile). +/// How many models profile pr's tier map holds: 1 to MODEL_STEPS (0 for no profile); the first empty step ends it. pub fn profile_models(pr: u32) -> u32 { if (pr >= PROFILE_COUNT) { return 0; } var tmp : [64]u8 = undefined; - if (csv_field(MODELS_SECOND, pr, tmp[0..64]) > 0) { return 2; } - return 1; + var n : u32 = 1; + while (n < MODEL_STEPS and tier_model_step(pr, TI_DEFAULT, n, tmp[0..64]) > 0) { n = n + 1; } + return n; } /// Does the model at `step` have a successor in profile pr's tier map? @@ -891,24 +924,38 @@ pub fn claude_strips(name: []const u8) -> bool { // ---- the profile chain with numbered keys (gHashTag/t27#11224, FR-003) ------------------------------------ // Free Claude Code walks an ordered fallback list when a request fails after its retries, and each fallback -// entry may name a key number of its provider (key 1 first). Here the list is PROFILE_NAMES, walked at pane -// spawn and pane failover (the failover decision is below), forward only: a profile that failed once is not -// tried again by the same pane, so the walk always ends. - -/// The first profile after `after` (PR_NONE = from the start) whose provider key is stored and has not -/// failed (bad bit set); PR_NONE when the chain is over. Any other `after` out of range ends the chain. +// entry may name a key number of its provider (key 1 first). Here the list is the profiles in tier order +// (PROFILE_TIER: the free z.ai tier, OpenRouter's free tier, then the paid profiles; the index breaks a tie), +// walked at pane spawn and pane failover (the failover decision is below), forward only: a profile that +// failed once is not tried again by the same pane, so the walk always ends. + +/// The place of profile pr in the chain: its tier first, its index second; past every profile for a number +/// that is none. +pub fn profile_rank(pr: u32) -> u32 { + if (pr >= PROFILE_COUNT) { return CT_COUNT * PROFILE_COUNT; } + return PROFILE_TIER[pr] * PROFILE_COUNT + pr; +} + +/// The first profile after `after` (PR_NONE = from the start) in chain order (profile_rank) whose provider key +/// is stored and has not failed (bad bit set); PR_NONE when the chain is over. A profile whose provider holds +/// no key is skipped, so a tier with no key (no OpenRouter key stored) costs the pane nothing. Any other +/// `after` out of range ends the chain. pub fn next_profile(after: u32, stored: u32, bad: u32) -> u32 { - var pr : u32 = 0; + var floor : u32 = 0; if (after != PR_NONE) { if (after >= PROFILE_COUNT) { return PR_NONE; } - pr = after + 1; + floor = profile_rank(after) + 1; } + var best : u32 = PR_NONE; + var pr : u32 = 0; while (pr < PROFILE_COUNT) { const bit : u32 = PROFILE_PROVIDER[pr]; - if ((stored >> bit) % 2 == 1 and (bad >> bit) % 2 == 0) { return pr; } + if ((stored >> bit) % 2 == 1 and (bad >> bit) % 2 == 0 and profile_rank(pr) >= floor) { + if (best == PR_NONE or profile_rank(pr) < profile_rank(best)) { best = pr; } + } pr = pr + 1; } - return PR_NONE; + return best; } /// The profile a new claude pane gets: the first in order whose provider key is stored and has not @@ -992,12 +1039,33 @@ pub fn profile_why(stored: u32, bad: u32, out: []u8) -> usize { const l : usize = profile_name(pr, tmp[0..24]); n = put_bytes(out, n, tmp[0..l]); if (pr == PR_ZAI) { return put_bytes(out, n, ", the first key in the chain order"); } - if (pr > PR_ANTHROPIC) { return put_bytes(out, n, ", no earlier profile has a usable key"); } + if (PROFILE_TIER[pr] > CT_OR_FREE) { return put_bytes(out, n, ", no free profile has a usable key"); } const first : u32 = PROFILE_PROVIDER[PR_ZAI]; if ((stored >> first) % 2 == 0) { return put_bytes(out, n, ", no z.ai key stored"); } return put_bytes(out, n, ", z.ai failed its check"); } +// ---- the request for the key of the OpenRouter tier (gHashTag/t27#11285, FR-003) --------------------------- +// The OpenRouter tier needs a key the owner stores in the KEYS tab (Keychain service ai.t27.os, account +// "openrouter"); until then next_profile skips it. The KEYS tab shows the request line below next to the +// provider's row while needs_key_request says so; nothing here holds or reads a key. + +/// The line the KEYS tab shows for a provider whose key the free chain is waiting for. +pub const KEY_REQUEST_OPENROUTER : str = "Add an OpenRouter key to use its free models"; + +/// Does provider p owe the person a request for a key: OpenRouter's free tier is in the chain and no key of it is +/// stored (`stored_count` is how many keys the table holds for p, key_count_text's n). Every other provider +/// either has no tier waiting on it or is covered by the old "no key stored" text. +pub fn needs_key_request(p: u32, stored_count: u32) -> bool { + return p == P_OPENROUTER and stored_count == 0; +} + +/// The request line for provider p (out needs 48 bytes); 0 when needs_key_request is false. +pub fn key_request_text(p: u32, stored_count: u32, out: []u8) -> usize { + if (needs_key_request(p, stored_count) == false) { return 0; } + return put_bytes(out, 0, KEY_REQUEST_OPENROUTER); +} + // ---- the pane failover: what to do when a claude pane shows a provider error (gHashTag/t27#11224, FR-004) -- // Owner, 2026-10-20: panes that start Claude Code inherited a provider that answered 429 "Insufficient // balance" or "organization has disabled Claude subscription access". Free Claude Code solves this inside a @@ -1030,9 +1098,9 @@ pub const FA_GIVE_UP : u32 = 3; /// model the pane ran. Only pty_logic plan_failover gives it (fo_next_model); fo_action never does. pub const FA_RESPAWN_MODEL : u32 = 4; -/// At most this many failovers per pane: every key of a provider on each of the MODEL_STEPS models of its tier -/// (MAX_KEYS * 2 = 16), then every profile once, and a few spare. -pub const MAX_FAILOVERS : u32 = 24; +/// At most this many failovers per pane: every key of each free provider on each model of its tier (z.ai 8 keys on +/// 2 models, OpenRouter 8 keys on 3 models: 8 * 5 = 40), then every profile once, and a few spare. +pub const MAX_FAILOVERS : u32 = 48; /// The index of the first occurrence of needle in hay at or after `from`; hay.len + 1 when absent. fn find_bytes(hay: []const u8, needle: []const u8, from: usize) -> usize { @@ -1355,15 +1423,26 @@ test the_default_z_ai_model_is_the_free_flash_that_answered_on_every_key { test a_profile_knows_how_many_models_its_tier_holds { assert profile_models(PR_ZAI) == 2; assert profile_models(PR_ANTHROPIC) == 1; - assert profile_models(PR_OPENROUTER) == 1; + assert profile_models(PR_OPENROUTER) == 3; assert profile_models(PR_OLLAMA_CLOUD) == 1; assert profile_models(PR_NONE) == 0; assert model_left(PR_ZAI, 0); assert model_left(PR_ZAI, 1) == false; assert model_left(PR_ANTHROPIC, 0) == false; + assert model_left(PR_OPENROUTER, 0); + assert model_left(PR_OPENROUTER, 1); + assert model_left(PR_OPENROUTER, 2) == false; assert model_left(PR_NONE, 0) == false; - assert MODEL_STEPS == 2; + assert MODEL_STEPS == 3; assert csv_fields(MODELS_SECOND) == PROFILE_COUNT; + assert csv_fields(MODELS_THIRD) == PROFILE_COUNT; + // a step is empty from the first gap on: no profile names a third model without a second + var pr : u32 = 0; + var tmp : [64]u8 = undefined; + while (pr < PROFILE_COUNT) { + if (csv_field(MODELS_THIRD, pr, tmp[0..64]) > 0) { assert csv_field(MODELS_SECOND, pr, tmp[0..64]) > 0; } + pr = pr + 1; + } } test the_tier_variables_of_a_pane_name_the_model_of_its_step { @@ -1400,10 +1479,18 @@ test a_restored_or_failed_over_pane_runs_the_model_its_profile_serves_never_the_ assert n == 0; n = model_for_profile(PR_ANTHROPIC, 0, "glm-4.7-flash", o[0..64]); assert n == 0; - n = model_for_profile(PR_OPENROUTER, 0, "glm-5.3-flash", o[0..64]); - assert n == 0; n = model_for_profile(PR_ANTHROPIC, 0, "", o[0..64]); assert n == 0; + // OpenRouter's free tier names a model at each of its three steps, whatever the pane carried over + n = model_for_profile(PR_OPENROUTER, 0, "glm-5.3-flash", o[0..64]); + assert n == 31 and begins_with(o[0..n], "minimax/minimax-m3.1-flash:free"); + n = model_for_profile(PR_OPENROUTER, 1, "glm-4.7-flash", o[0..64]); + assert n == 33 and begins_with(o[0..n], "nvidia/nemotron-3.5-super-vl:free"); + n = model_for_profile(PR_OPENROUTER, 2, "", o[0..64]); + assert n == 34 and begins_with(o[0..n], "nvidia/nemotron-3.5-lightning:free"); + // a free id of OpenRouter's has no place on the Anthropic API a pane fails over to after it + n = model_for_profile(PR_ANTHROPIC, 0, "minimax/minimax-m3.1-flash:free", o[0..64]); + assert n == 0; // no profile (a pane that has no key): nothing to move it to, the saved model stays n = model_for_profile(PR_NONE, 0, "glm-5.2", o[0..64]); assert n == 7 and begins_with(o[0..n], "glm-5.2"); @@ -1436,27 +1523,63 @@ test the_zai_variables_come_in_slot_order_with_the_sonnet_and_opus_tiers { assert profile_var_is_key(PR_ZAI, 5) == false; } -test the_openrouter_profile_sets_its_three_tiers_and_an_empty_api_key { +test the_openrouter_profile_reaches_claude_code_by_the_same_variables_and_names_a_free_id_at_every_tier { var o : [64]u8 = undefined; - assert profile_var_count(PR_OPENROUTER) == 7; + // the base URL, the key variable and the empty API key are the ones the profile always had + assert profile_var_count(PR_OPENROUTER) == 8; + assert profile_var_name(PR_OPENROUTER, 0, o[0..64]) == 18; assert profile_var_text(PR_OPENROUTER, 0, o[0..64]) == 25; assert o[8] == 111 and o[24] == 105; assert profile_var_is_key(PR_OPENROUTER, 1); assert profile_var_name(PR_OPENROUTER, 1, o[0..64]) == 20; - assert profile_var_name(PR_OPENROUTER, 2, o[0..64]) == 26; - assert profile_var_text(PR_OPENROUTER, 2, o[0..64]) == 30; - assert o[0] == 126 and o[11] == 99; - assert profile_var_name(PR_OPENROUTER, 3, o[0..64]) == 29; - assert profile_var_text(PR_OPENROUTER, 3, o[0..64]) == 30; - assert profile_var_name(PR_OPENROUTER, 4, o[0..64]) == 30; - assert profile_var_text(PR_OPENROUTER, 4, o[0..64]) == 35; - assert o[34] == 93; - assert profile_var_name(PR_OPENROUTER, 5, o[0..64]) == 28; - assert profile_var_text(PR_OPENROUTER, 5, o[0..64]) == 33; - assert profile_var_name(PR_OPENROUTER, 6, o[0..64]) == 17; - assert profile_var_is_key(PR_OPENROUTER, 6) == false; - assert profile_var_text(PR_OPENROUTER, 6, o[0..64]) == 0; - assert tier_model(PR_OPENROUTER, TI_DEFAULT, o[0..64]) == 0; + assert profile_var_text(PR_OPENROUTER, 1, o[0..64]) == 0; + assert profile_var_name(PR_OPENROUTER, 7, o[0..64]) == 17; + assert profile_var_is_key(PR_OPENROUTER, 7) == false; + assert profile_var_text(PR_OPENROUTER, 7, o[0..64]) == 0; + // the model variables (default, small-fast, haiku, sonnet, opus) all name the step's free id: no tier + // asks OpenRouter for a paid model while the free ones answer + assert profile_var_name(PR_OPENROUTER, 2, o[0..64]) == 15; + assert profile_var_name(PR_OPENROUTER, 3, o[0..64]) == 26; + assert profile_var_name(PR_OPENROUTER, 4, o[0..64]) == 29; + assert profile_var_name(PR_OPENROUTER, 5, o[0..64]) == 30; + assert profile_var_name(PR_OPENROUTER, 6, o[0..64]) == 28; + var k : u32 = 2; + while (k < 7) { + var n : usize = profile_var_text_step(PR_OPENROUTER, k, 0, o[0..64]); + assert n == 31 and begins_with(o[0..n], "minimax/minimax-m3.1-flash:free"); + n = profile_var_text_step(PR_OPENROUTER, k, 1, o[0..64]); + assert n == 33 and begins_with(o[0..n], "nvidia/nemotron-3.5-super-vl:free"); + n = profile_var_text_step(PR_OPENROUTER, k, 2, o[0..64]); + assert n == 34 and begins_with(o[0..n], "nvidia/nemotron-3.5-lightning:free"); + assert profile_var_text_step(PR_OPENROUTER, k, MODEL_STEPS, o[0..64]) == 0; + k = k + 1; + } + assert tier_model(PR_OPENROUTER, TI_DEFAULT, o[0..64]) == 31; +} + +test every_openrouter_model_id_is_a_free_one_and_no_other_profile_names_one { + var o : [64]u8 = undefined; + var st : u32 = 0; + while (st < MODEL_STEPS) { + var ti : u32 = 0; + while (ti < TIER_COUNT) { + const n : usize = tier_model_step(PR_OPENROUTER, ti, st, o[0..64]); + assert n > 6; + assert o[n - 5] == 58 and o[n - 4] == 102 and o[n - 3] == 114 and o[n - 2] == 101 and o[n - 1] == 101; + ti = ti + 1; + } + st = st + 1; + } + var pr : u32 = 0; + while (pr < PROFILE_COUNT) { + var s2 : u32 = 0; + while (s2 < MODEL_STEPS and pr != PR_OPENROUTER) { + const m : usize = tier_model_step(pr, TI_DEFAULT, s2, o[0..64]); + assert has_bytes(o[0..m], ":free") == false; + s2 = s2 + 1; + } + pr = pr + 1; + } } test the_ollama_cloud_profile_names_one_cloud_model_for_every_tier { @@ -1476,15 +1599,17 @@ test the_ollama_cloud_profile_names_one_cloud_model_for_every_tier { assert profile_var_text(PR_OLLAMA_CLOUD, 7, o[0..64]) == 0; } -test the_new_profiles_follow_the_old_two_and_wait_for_their_own_key { +test the_new_profiles_follow_the_free_zai_tier_and_wait_for_their_own_key { assert pick_profile(8, 0) == PR_OPENROUTER; assert pick_profile(32, 0) == PR_OLLAMA_CLOUD; assert pick_profile(40, 0) == PR_OPENROUTER; assert pick_profile(40, 8) == PR_OLLAMA_CLOUD; assert pick_profile(40, 40) == PR_NONE; assert pick_profile(57, 0) == PR_ZAI; - assert pick_profile(57, 16) == PR_ANTHROPIC; + // z.ai failed: OpenRouter's free tier comes before the paid Anthropic API, whatever the profile numbers say + assert pick_profile(57, 16) == PR_OPENROUTER; assert pick_profile(57, 17) == PR_OPENROUTER; + assert pick_profile(57, 24) == PR_ANTHROPIC; assert pick_profile(57, 25) == PR_OLLAMA_CLOUD; assert pick_profile(4, 0) == PR_NONE; assert pick_profile(448, 0) == PR_NONE; @@ -1492,15 +1617,17 @@ test the_new_profiles_follow_the_old_two_and_wait_for_their_own_key { test the_chain_goes_forward_only_and_skips_a_missing_or_bad_key { assert next_profile(PR_NONE, 57, 0) == PR_ZAI; - assert next_profile(PR_ZAI, 57, 0) == PR_ANTHROPIC; - assert next_profile(PR_ANTHROPIC, 57, 0) == PR_OPENROUTER; - assert next_profile(PR_OPENROUTER, 57, 0) == PR_OLLAMA_CLOUD; + assert next_profile(PR_ZAI, 57, 0) == PR_OPENROUTER; + assert next_profile(PR_OPENROUTER, 57, 0) == PR_ANTHROPIC; + assert next_profile(PR_ANTHROPIC, 57, 0) == PR_OLLAMA_CLOUD; assert next_profile(PR_OLLAMA_CLOUD, 57, 0) == PR_NONE; assert next_profile(PR_ZAI, 57, 1) == PR_OPENROUTER; assert next_profile(PR_ZAI, 17, 0) == PR_ANTHROPIC; assert next_profile(PR_ANTHROPIC, 17, 0) == PR_NONE; - assert next_profile(PR_OPENROUTER, 25, 0) == PR_NONE; - assert next_profile(PR_OPENROUTER, 57, 32) == PR_NONE; + assert next_profile(PR_OPENROUTER, 25, 0) == PR_ANTHROPIC; + assert next_profile(PR_OPENROUTER, 57, 32) == PR_ANTHROPIC; + assert next_profile(PR_ANTHROPIC, 57, 32) == PR_NONE; + assert next_profile(PR_OPENROUTER, 57, 33) == PR_NONE; assert next_profile(PR_NONE, 0, 0) == PR_NONE; assert next_profile(PROFILE_COUNT, 57, 0) == PR_NONE; assert next_profile(7, 57, 0) == PR_NONE; @@ -1523,7 +1650,7 @@ test a_profile_is_never_offered_twice_by_one_walk { var steps : u32 = 0; var prev : u32 = 255; while (pr != PR_NONE) { - if (prev != 255) { assert pr > prev; } + if (prev != 255) { assert profile_rank(pr) > profile_rank(prev); } prev = pr; pr = next_profile(pr, 57, 0); steps = steps + 1; @@ -1531,6 +1658,102 @@ test a_profile_is_never_offered_twice_by_one_walk { assert steps == PROFILE_COUNT; } +test the_tier_order_is_free_zai_then_free_openrouter_then_paid { + assert PROFILE_TIER.len == PROFILE_COUNT; + assert PROFILE_TIER[PR_ZAI] == CT_ZAI_FREE; + assert PROFILE_TIER[PR_OPENROUTER] == CT_OR_FREE; + assert PROFILE_TIER[PR_ANTHROPIC] == CT_PAID; + assert PROFILE_TIER[PR_OLLAMA_CLOUD] == CT_PAID; + assert CT_ZAI_FREE < CT_OR_FREE and CT_OR_FREE < CT_PAID and CT_PAID + 1 == CT_COUNT; + // the rank is the chain place: tier first, index second, and no two profiles share one + assert profile_rank(PR_ZAI) < profile_rank(PR_OPENROUTER); + assert profile_rank(PR_OPENROUTER) < profile_rank(PR_ANTHROPIC); + assert profile_rank(PR_ANTHROPIC) < profile_rank(PR_OLLAMA_CLOUD); + var a : u32 = 0; + while (a < PROFILE_COUNT) { + var b : u32 = a + 1; + while (b < PROFILE_COUNT) { + assert profile_rank(a) != profile_rank(b); + b = b + 1; + } + a = a + 1; + } + assert profile_rank(PR_NONE) >= profile_rank(PR_OLLAMA_CLOUD); + // a new pane with a key for every profile starts on z.ai, and each failure moves it one tier on + assert pick_profile(57, 0) == PR_ZAI; + assert next_profile(pick_profile(57, 0), 57, 16) == PR_OPENROUTER; + assert next_profile(PR_OPENROUTER, 57, 24) == PR_ANTHROPIC; + // the paid profiles are never offered while a free tier still has a usable key + var stored : u32 = 0; + while (stored < 64) { + const first : u32 = pick_profile(stored, 0); + if (first != PR_NONE and PROFILE_TIER[first] == CT_PAID) { + assert (stored >> PROFILE_PROVIDER[PR_ZAI]) % 2 == 0; + assert (stored >> PROFILE_PROVIDER[PR_OPENROUTER]) % 2 == 0; + } + stored = stored + 1; + } +} + +test with_no_openrouter_key_stored_the_free_tier_is_skipped_and_never_an_error_loop { + // 17 = z.ai + Anthropic, no OpenRouter key: z.ai leads straight to the paid profile + assert next_profile(PR_ZAI, 17, 0) == PR_ANTHROPIC; + assert pick_profile(17, 16) == PR_ANTHROPIC; + assert fo_target(FC_NO_BALANCE, PR_ZAI, 17, 0, 0, false) == PR_ANTHROPIC; + // 16 = z.ai alone: nothing is left after it, the pane gives up and says so + assert next_profile(PR_ZAI, 16, 0) == PR_NONE; + assert fo_action(FC_NO_BALANCE, PR_ZAI, 16, 0, 0, false) == FA_GIVE_UP; + assert fo_target(FC_NO_BALANCE, PR_ZAI, 16, 0, 0, false) == PR_NONE; + // 8 = an OpenRouter key alone: a pane with no profile starts on it + assert pick_profile(8, 0) == PR_OPENROUTER; + // with the key stored the tier is offered, and its own failure hands the pane to the paid profile + assert next_profile(PR_ZAI, 25, 0) == PR_OPENROUTER; + assert fo_target(FC_RATE_LIMITED, PR_ZAI, 25, 0, 0, false) == PR_OPENROUTER; + assert fo_target(FC_RATE_LIMITED, PR_OPENROUTER, 25, 16, 1, false) == PR_ANTHROPIC; + // an OpenRouter key that failed its check is skipped like a missing one + assert next_profile(PR_ZAI, 25, 8) == PR_ANTHROPIC; + assert pick_profile(25, 24) == PR_ANTHROPIC; + // the walk ends in every combination of stored keys: no more steps than profiles + var stored : u32 = 0; + while (stored < 64) { + var pr : u32 = next_profile(PR_NONE, stored, 0); + var steps : u32 = 0; + while (pr != PR_NONE and steps < 10) { + pr = next_profile(pr, stored, 0); + steps = steps + 1; + } + assert steps <= PROFILE_COUNT; + stored = stored + 1; + } +} + +test an_openrouter_request_is_owed_exactly_when_its_key_is_absent { + var o : [48]u8 = undefined; + assert needs_key_request(P_OPENROUTER, 0); + assert needs_key_request(P_OPENROUTER, 1) == false; + assert needs_key_request(P_OPENROUTER, MAX_KEYS) == false; + // no other provider is asked for: z.ai and the paid ones keep the old "no key stored" row + var p : u32 = 0; + while (p < PROVIDER_COUNT) { + if (p != P_OPENROUTER) { + assert needs_key_request(p, 0) == false; + assert needs_key_request(p, 1) == false; + } + p = p + 1; + } + // the text is one short English line and is given only while the request is owed + const n : usize = key_request_text(P_OPENROUTER, 0, o[0..48]); + assert n == 44; + assert begins_with(o[0..n], "Add an OpenRouter key to use its free models"); + assert key_request_text(P_OPENROUTER, 1, o[0..48]) == 0; + assert key_request_text(P_ZAI, 0, o[0..48]) == 0; + var i : usize = 0; + while (i < n) { + assert o[i] >= 32 and o[i] < 127; + i = i + 1; + } +} + test rotation_can_skip_the_key_that_just_failed { var used : [3]u64 = [50, 10, 30]; var rest : [3]u64 = [0, 0, 0]; @@ -1641,12 +1864,12 @@ test an_overloaded_key_with_a_spare_swaps_the_key_and_keeps_the_profile { assert fo_target(FC_OVERLOADED, PR_ZAI, 57, 0, 0, true) == PR_ZAI; // no other key usable now: the pane leaves the profile, as for a throttled key with no spare assert fo_action(FC_OVERLOADED, PR_ZAI, 57, 0, 0, false) == FA_RESPAWN_NEXT; - assert fo_target(FC_OVERLOADED, PR_ZAI, 57, 0, 0, false) == PR_ANTHROPIC; + assert fo_target(FC_OVERLOADED, PR_ZAI, 57, 0, 0, false) == PR_OPENROUTER; assert fo_action(FC_OVERLOADED, PR_ZAI, 16, 0, 0, false) == FA_GIVE_UP; assert fo_action(FC_OVERLOADED, PR_NONE, 57, 0, 0, true) == FA_RESPAWN_NEXT; assert fo_action(FC_OVERLOADED, PR_ZAI, 57, 0, MAX_FAILOVERS, true) == FA_GIVE_UP; var done : u32 = 0; - while (done < 40 and fo_action(FC_OVERLOADED, PR_ZAI, 57, 0, done, true) == FA_RESPAWN_KEY) { + while (done < 100 and fo_action(FC_OVERLOADED, PR_ZAI, 57, 0, done, true) == FA_RESPAWN_KEY) { done = done + 1; } assert done == MAX_FAILOVERS; @@ -1821,11 +2044,14 @@ test no_error_or_an_unknown_one_leaves_the_pane_alone { test a_dead_profile_hands_the_pane_to_the_next_one { assert fo_action(FC_NO_BALANCE, PR_ZAI, 57, 0, 0, false) == FA_RESPAWN_NEXT; - assert fo_target(FC_NO_BALANCE, PR_ZAI, 57, 0, 0, false) == PR_ANTHROPIC; - assert fo_target(FC_LOGIN_REJECTED, PR_ANTHROPIC, 57, 16, 1, false) == PR_OPENROUTER; + // the free tiers first: z.ai, then OpenRouter's `:free` ids, then the paid profiles + assert fo_target(FC_NO_BALANCE, PR_ZAI, 57, 0, 0, false) == PR_OPENROUTER; + assert fo_target(FC_RATE_LIMITED, PR_OPENROUTER, 57, 16, 1, false) == PR_ANTHROPIC; + assert fo_target(FC_LOGIN_REJECTED, PR_ANTHROPIC, 57, 24, 2, false) == PR_OLLAMA_CLOUD; assert fo_target(FC_RATE_LIMITED, PR_OPENROUTER, 57, 17, 2, false) == PR_OLLAMA_CLOUD; - assert fo_target(FC_NO_BALANCE, PR_ZAI, 41, 0, 0, false) == PR_ANTHROPIC; + assert fo_target(FC_NO_BALANCE, PR_ZAI, 41, 0, 0, false) == PR_OPENROUTER; assert fo_target(FC_NO_BALANCE, PR_ZAI, 40, 0, 0, false) == PR_OPENROUTER; + assert fo_target(FC_NO_BALANCE, PR_ZAI, 33, 0, 0, false) == PR_ANTHROPIC; } test a_throttled_key_with_a_spare_swaps_the_key_and_keeps_the_profile { @@ -1889,7 +2115,7 @@ test a_pane_that_fails_on_every_profile_ends_in_give_up_after_each_was_tried_onc test a_host_that_always_reports_a_spare_key_still_cannot_loop { var done : u32 = 0; - while (done < 40 and fo_action(FC_RATE_LIMITED, PR_ZAI, 57, 0, done, true) == FA_RESPAWN_KEY) { + while (done < 100 and fo_action(FC_RATE_LIMITED, PR_ZAI, 57, 0, done, true) == FA_RESPAWN_KEY) { done = done + 1; } assert done == MAX_FAILOVERS; @@ -1931,14 +2157,44 @@ test a_pane_with_no_key_left_on_its_model_takes_the_next_model_of_the_tier_befor assert fo_next_model(FC_NONE, PR_ZAI, 0, 3, false) == false; assert fo_next_model(FC_OTHER, PR_ZAI, 0, 3, false) == false; assert fo_next_model(FC_NO_BALANCE, PR_ZAI, 0, MAX_FAILOVERS, false) == false; - // the cap leaves room for every key of a provider on each model of its tier, then every profile - assert MAX_FAILOVERS >= MAX_KEYS * MODEL_STEPS + PROFILE_COUNT; + // OpenRouter's free tier holds three models: the next one at steps 0 and 1, none after the last + assert fo_next_model(FC_RATE_LIMITED, PR_OPENROUTER, 0, 3, false); + assert fo_next_model(FC_OVERLOADED, PR_OPENROUTER, 1, 3, false); + assert fo_next_model(FC_RATE_LIMITED, PR_OPENROUTER, 2, 3, false) == false; + assert fo_next_model(FC_RATE_LIMITED, PR_OPENROUTER, 0, 3, true) == false; + // the cap leaves room for every key of each free provider on each model of its tier, then every profile + assert MAX_FAILOVERS >= MAX_KEYS * (profile_models(PR_ZAI) + profile_models(PR_OPENROUTER)) + PROFILE_COUNT; var o : [128]u8 = undefined; const n : usize = fo_why(FC_NO_BALANCE, PR_ZAI, FA_RESPAWN_MODEL, PR_ZAI, 3, o[0..128]); assert has_bytes(o[0..n], "claude: zai out of balance, every key tried, same profile on the next model"); assert n < 128; } +test a_pane_whose_zai_keys_all_rest_moves_to_openrouter_and_after_its_free_ids_to_the_paid_profile { + // the decisions a pane meets in order, with every key of the tier failing: stored 57 = every profile has a key + // z.ai, last key, last model: no key and no model left, so the next tier + assert fo_next_model(FC_OVERLOADED, PR_ZAI, 1, 5, false) == false; + assert fo_action(FC_OVERLOADED, PR_ZAI, 57, 0, 5, false) == FA_RESPAWN_NEXT; + assert fo_target(FC_OVERLOADED, PR_ZAI, 57, 0, 5, false) == PR_OPENROUTER; + // OpenRouter's only key failed: its three free ids in turn, then the paid profile + assert fo_next_model(FC_RATE_LIMITED, PR_OPENROUTER, 0, 6, false); + assert fo_next_model(FC_RATE_LIMITED, PR_OPENROUTER, 1, 7, false); + assert fo_next_model(FC_RATE_LIMITED, PR_OPENROUTER, 2, 8, false) == false; + assert fo_action(FC_RATE_LIMITED, PR_OPENROUTER, 57, 16, 8, false) == FA_RESPAWN_NEXT; + assert fo_target(FC_RATE_LIMITED, PR_OPENROUTER, 57, 16, 8, false) == PR_ANTHROPIC; + // with no OpenRouter key stored (stored 49 = z.ai, Anthropic, Ollama): z.ai goes to the paid profile + assert fo_target(FC_OVERLOADED, PR_ZAI, 49, 0, 5, false) == PR_ANTHROPIC; + // with no paid key either (stored 16 or 24) the last free tier ends in the give-up line + assert fo_action(FC_OVERLOADED, PR_ZAI, 16, 0, 5, false) == FA_GIVE_UP; + assert fo_action(FC_RATE_LIMITED, PR_OPENROUTER, 24, 16, 8, false) == FA_GIVE_UP; + var o : [128]u8 = undefined; + const n : usize = fo_why(FC_OVERLOADED, PR_ZAI, FA_RESPAWN_NEXT, PR_OPENROUTER, 5, o[0..128]); + assert has_bytes(o[0..n], "claude: zai overloaded (529), respawn on openrouter"); + const g : usize = fo_why(FC_RATE_LIMITED, PR_OPENROUTER, FA_GIVE_UP, PR_NONE, 8, o[0..128]); + assert has_bytes(o[0..g], "claude: openrouter rate limited (429), no profile left in the chain, the pane stays"); + assert g < 128; +} + test the_profile_tables_have_one_field_per_profile { assert csv_fields(PROFILE_NAMES) == PROFILE_COUNT; assert csv_fields(MODELS_DEFAULT) == PROFILE_COUNT; @@ -1946,10 +2202,12 @@ test the_profile_tables_have_one_field_per_profile { assert csv_fields(MODELS_SONNET) == PROFILE_COUNT; assert csv_fields(MODELS_OPUS) == PROFILE_COUNT; assert csv_fields(MODELS_SECOND) == PROFILE_COUNT; + assert csv_fields(MODELS_THIRD) == PROFILE_COUNT; assert csv_fields(PROFILE_BASE_URLS) == PROFILE_COUNT; assert csv_fields(PROFILE_KEY_VARS) == PROFILE_COUNT; assert csv_fields(TIER_VARS) == TIER_COUNT; assert PROFILE_PROVIDER.len == PROFILE_COUNT; + assert PROFILE_TIER.len == PROFILE_COUNT; assert PROFILE_EMPTY_API_KEY.len == PROFILE_COUNT; assert PS_TIER0 + TIER_COUNT == PS_EMPTY_KEY; assert PS_EMPTY_KEY + 1 == PS_COUNT; @@ -2011,6 +2269,16 @@ test the_why_line_names_the_profile_and_never_a_key { assert o[8] == 111 and o[9] == 112; assert profile_why(32, 0, o[0..96]) > 30; assert o[8] == 111 and o[9] == 108; + // OpenRouter's free tier follows z.ai and says why z.ai was passed over; a paid profile says no free one is usable + var n : usize = profile_why(24, 16, o[0..96]); + assert has_bytes(o[0..n], "claude: openrouter, z.ai failed its check"); + n = profile_why(8, 0, o[0..96]); + assert has_bytes(o[0..n], "claude: openrouter, no z.ai key stored"); + n = profile_why(17, 16, o[0..96]); + assert has_bytes(o[0..n], "claude: anthropic-api, no free profile has a usable key"); + n = profile_why(32, 0, o[0..96]); + assert has_bytes(o[0..n], "claude: ollama-cloud, no free profile has a usable key"); + assert n < 96; } test the_zai_check_is_one_message_to_the_flash_model { diff --git a/specs/t27os/pty_logic.t27 b/specs/t27os/pty_logic.t27 index 35e4bb833f..2f396a230f 100644 --- a/specs/t27os/pty_logic.t27 +++ b/specs/t27os/pty_logic.t27 @@ -97,11 +97,13 @@ use t27os::keys::PROFILE_PROVIDER; use t27os::keys::PR_NONE; use t27os::keys::PR_ZAI; use t27os::keys::PR_ANTHROPIC; +use t27os::keys::PR_OPENROUTER; use t27os::keys::MAX_FAILOVERS; use t27os::keys::agent_uses; use t27os::keys::spawn_mask; use t27os::keys::P_ZAI; use t27os::keys::P_ANTHROPIC; +use t27os::keys::P_OPENROUTER; use t27os::keys::P_OPENAI; use t27os::keys::FC_OTHER; use t27os::keys::FC_LOGIN_REJECTED; @@ -1790,6 +1792,140 @@ test every_key_resting_for_an_empty_balance_moves_the_pane_on_and_no_dead_key_is assert st[G_PANEK + PK_STOP] == t + 600000; } +// ---- the tier order on a pane: z.ai free, then OpenRouter's free ids, then the paid profiles (gHashTag/t27#11285) ---- + +/// The table with a key for every tier of the chain: three z.ai keys, one OpenRouter key, one Anthropic key. +fn t_chain(st: []u64) -> void { + t_zai3(st); + t_key(st, P_OPENROUTER, 0); +} + +/// One failure of class `class` on pane 0 at `t` ms: the failover, then the relaunch's key and profile choice +/// half a second later unless the pane gave up. Returns the action. +fn t_fail(st: []u64, class: u32, t: u64) -> u32 { + var why : [128]u8 = undefined; + var wl : [2]u64 = undefined; + const a : u32 = plan_failover(st, 0, class, t, why[0..128], wl[0..2]); + if (a != FA_GIVE_UP) { plan_pane_keys(st, 0, 0, t + 500); } + return a; +} + +test when_every_zai_key_rests_the_pane_moves_to_openrouters_free_ids_and_then_to_the_paid_profile { + var st : [512]u64 = undefined; + var o : [64]u8 = undefined; + t_chain(st[0..512]); + t_restored(st[0..512], 0); + plan_pane_keys(st[0..512], 0, 0, 1000000); + assert t_prof(st[0..512], 0) == PR_ZAI; + // tier 1, z.ai: three keys on glm-4.5-flash, then the same three on glm-4.7-flash, every one overloaded + assert t_fail(st[0..512], FC_OVERLOADED, 1001000) == FA_RESPAWN_KEY; + assert t_fail(st[0..512], FC_OVERLOADED, 1002000) == FA_RESPAWN_KEY; + assert t_fail(st[0..512], FC_OVERLOADED, 1003000) == FA_RESPAWN_MODEL; + assert t_fail(st[0..512], FC_OVERLOADED, 1004000) == FA_RESPAWN_KEY; + assert t_fail(st[0..512], FC_OVERLOADED, 1005000) == FA_RESPAWN_KEY; + assert t_prof(st[0..512], 0) == PR_ZAI; + assert pane_mstep_get(st[0..512], 0) == 1; + // the last z.ai key on the last model: tier 2, OpenRouter, on its first free id with its own key + assert t_fail(st[0..512], FC_OVERLOADED, 1006000) == FA_RESPAWN_NEXT; + assert t_prof(st[0..512], 0) == PR_OPENROUTER; + assert pane_mstep_get(st[0..512], 0) == 0; + assert pane_key_get(st[0..512], 0, P_OPENROUTER) == 1; + assert pane_key_get(st[0..512], 0, P_ZAI) == 0; + assert st[G_PANEK + PK_BAD] == 16; + var n : usize = launch_model(st[0..512], 0, 0, false, "glm-4.7-flash", o[0..64]); + assert n == 31 and begins_with(o[0..n], "minimax/minimax-m3.1-flash:free"); + // the environment the pane gets: ANTHROPIC_MODEL (variable 2 of the profile) names the same id + n = profile_var_text_step(PR_OPENROUTER, 2, pane_mstep_get(st[0..512], 0), o[0..64]); + assert n == 31 and begins_with(o[0..n], "minimax/minimax-m3.1-flash:free"); + // the only OpenRouter key is throttled: its second and third free id, the key free again for each + assert t_fail(st[0..512], FC_RATE_LIMITED, 1007000) == FA_RESPAWN_MODEL; + assert t_prof(st[0..512], 0) == PR_OPENROUTER; + assert pane_mstep_get(st[0..512], 0) == 1; + assert pane_key_get(st[0..512], 0, P_OPENROUTER) == 1; + n = launch_model(st[0..512], 0, 0, false, "minimax/minimax-m3.1-flash:free", o[0..64]); + assert n == 33 and begins_with(o[0..n], "nvidia/nemotron-3.5-super-vl:free"); + assert t_fail(st[0..512], FC_RATE_LIMITED, 1008000) == FA_RESPAWN_MODEL; + assert pane_mstep_get(st[0..512], 0) == 2; + n = launch_model(st[0..512], 0, 0, false, "nvidia/nemotron-3.5-super-vl:free", o[0..64]); + assert n == 34 and begins_with(o[0..n], "nvidia/nemotron-3.5-lightning:free"); + // both free tiers rest: only now the paid profile, whose model is not an OpenRouter id + assert t_fail(st[0..512], FC_RATE_LIMITED, 1009000) == FA_RESPAWN_NEXT; + assert t_prof(st[0..512], 0) == PR_ANTHROPIC; + assert pane_mstep_get(st[0..512], 0) == 0; + assert pane_key_get(st[0..512], 0, P_ANTHROPIC) == 1; + assert st[G_PANEK + PK_BAD] == 24; + assert st[G_PANEK + PK_DONE] == 9; + assert launch_model(st[0..512], 0, 0, false, "nvidia/nemotron-3.5-lightning:free", o[0..64]) == 0; + // the paid key fails too: no tier is left and the pane says so once + assert t_fail(st[0..512], FC_RATE_LIMITED, 1010000) == FA_GIVE_UP; + assert st[G_PANEK + PK_DONE] == 9; + assert st[G_PANEK + PK_STOP] == 1010000 + GIVEUP_QUIET_MS; +} + +test with_no_openrouter_key_stored_the_pane_skips_that_tier_to_the_paid_profile_or_the_give_up_line { + var st : [512]u64 = undefined; + var why : [128]u8 = undefined; + var wl : [2]u64 = undefined; + // z.ai and Anthropic keys, none of OpenRouter: the tier in the middle costs nothing + t_table(st[0..512]); + t_key(st[0..512], P_ZAI, 0); + t_key(st[0..512], P_ANTHROPIC, 0); + t_restored(st[0..512], 0); + plan_pane_keys(st[0..512], 0, 0, 1000000); + assert t_fail(st[0..512], FC_NO_BALANCE, 1001000) == FA_RESPAWN_MODEL; + assert t_fail(st[0..512], FC_NO_BALANCE, 1002000) == FA_RESPAWN_NEXT; + assert t_prof(st[0..512], 0) == PR_ANTHROPIC; + assert pane_key_get(st[0..512], 0, P_OPENROUTER) == 0; + assert st[G_PANEK + PK_BAD] == 16; + assert st[G_PANEK + PK_DONE] == 2; + // z.ai alone: nothing after it, the give-up line, no loop + t_table(st[0..512]); + t_key(st[0..512], P_ZAI, 0); + t_restored(st[0..512], 0); + plan_pane_keys(st[0..512], 0, 0, 1000000); + assert t_fail(st[0..512], FC_NO_BALANCE, 1001000) == FA_RESPAWN_MODEL; + const a : u32 = plan_failover(st[0..512], 0, FC_NO_BALANCE, 1002000, why[0..128], wl[0..2]); + assert a == FA_GIVE_UP; + assert has_bytes(why[0 .. @as(usize, @intCast(wl[0]))], "claude: zai out of balance, no profile left in the chain, the pane stays"); + assert st[G_PANEK + PK_DONE] == 1; + // the same pane once an OpenRouter key arrives (an OP_SETKEY) and its quiet time has passed: the free tier is offered + t_key(st[0..512], P_OPENROUTER, 0); + st[G_PANEK + PK_STOP] = 0; + const b : u32 = plan_failover(st[0..512], 0, FC_NO_BALANCE, 1003000, why[0..128], wl[0..2]); + assert b == FA_RESPAWN_NEXT; + assert t_prof(st[0..512], 0) == PR_OPENROUTER; + assert has_bytes(why[0 .. @as(usize, @intCast(wl[0]))], "claude: zai out of balance, respawn on openrouter"); + // an OpenRouter key alone ends in the give-up line after its three ids + t_table(st[0..512]); + t_key(st[0..512], P_OPENROUTER, 0); + t_restored(st[0..512], 0); + plan_pane_keys(st[0..512], 0, 0, 1000000); + assert t_prof(st[0..512], 0) == PR_OPENROUTER; + assert t_fail(st[0..512], FC_NO_BALANCE, 1001000) == FA_RESPAWN_MODEL; + assert t_fail(st[0..512], FC_NO_BALANCE, 1002000) == FA_RESPAWN_MODEL; + assert t_fail(st[0..512], FC_NO_BALANCE, 1003000) == FA_GIVE_UP; + assert st[G_PANEK + PK_DONE] == 2; +} + +test a_pane_on_openrouter_names_the_free_id_on_its_command_line_and_in_its_environment { + var st : [512]u64 = undefined; + var o : [64]u8 = undefined; + var cmd : [200]u8 = undefined; + t_table(st[0..512]); + t_key(st[0..512], P_OPENROUTER, 0); + t_restored(st[0..512], 0); + plan_pane_keys(st[0..512], 0, 0, 1000000); + assert t_prof(st[0..512], 0) == PR_OPENROUTER; + assert pane_key_get(st[0..512], 0, P_OPENROUTER) == 1; + const n : usize = launch_model(st[0..512], 0, 0, false, "glm-5.2", o[0..64]); + assert n == 31 and begins_with(o[0..n], "minimax/minimax-m3.1-flash:free"); + // the id keeps its slash and its colon on the command line (session_store model_ok), so the flag is not dropped + const c : usize = pane_command(0, R_RESUME, "5f1c9a00-aaaa-bbbb-cccc-0123456789ab", o[0..n], "/h", cmd[0..200]); + assert pl_count(cmd[0..c], "claude --resume 5f1c9a00-aaaa-bbbb-cccc-0123456789ab --model 'minimax/minimax-m3.1-flash:free' --settings /h/.t27os/hooks.json") == 1; + // a SPAWN's first start passes no model of its own + assert launch_model(st[0..512], 0, 0, true, "", o[0..64]) == 0; +} + test a_list_entry_carries_the_key_number_in_the_high_nibble { const w : u32 = pane_step(0, P_SPAWNED); assert list_flags_key(true, w, 0) == list_flags(true, w); diff --git a/specs/t27os/session_store.t27 b/specs/t27os/session_store.t27 index 0b968a1a3c..8959061198 100644 --- a/specs/t27os/session_store.t27 +++ b/specs/t27os/session_store.t27 @@ -178,8 +178,10 @@ pub fn sid_ok(s: []const u8) -> bool { return true; } -/// A model name is 1..MODEL_MAX characters of letters, digits and "-", ".", "_", "[", "]" (claude-opus-5-5, -/// opus[1m], gpt-5.1). build_command quotes it, so "[" never reaches the shell as a glob. "-" alone means none. +/// A model name is 1..MODEL_MAX characters of letters, digits and "-", ".", "_", "[", "]", "/" and ":" (claude-opus-5-5, +/// opus[1m], gpt-5.1, and a gateway's own id of a vendor, a slash and a tag: minimax/minimax-m3.1-flash:free, see +/// gHashTag/t27#11285). build_command quotes it, so "[" never reaches the shell as a glob and "/" and ":" are plain +/// bytes inside the quotes. "-" alone means none. pub fn model_ok(s: []const u8) -> bool { if (s.len == 0 or s.len > MODEL_MAX) { return false; } if (s.len == 1 and s[0] == SID_NONE) { return false; } @@ -187,7 +189,7 @@ pub fn model_ok(s: []const u8) -> bool { while (i < s.len) { const c : u8 = s[i]; const alnum : bool = (c >= 48 and c <= 57) or (c >= 65 and c <= 90) or (c >= 97 and c <= 122); - const mark : bool = c == 45 or c == 46 or c == 95 or c == 91 or c == 93; + const mark : bool = c == 45 or c == 46 or c == 95 or c == 91 or c == 93 or c == 47 or c == 58; if (!alnum and !mark) { return false; } i = i + 1; } @@ -547,6 +549,9 @@ test only_plain_model_names_reach_a_command_line { assert model_ok("claude-opus-5-5"); assert model_ok("opus[1m]"); assert model_ok("gpt-5.1_mini"); + // a gateway's own id: a vendor, a slash and a tag (OpenRouter's free tier, gHashTag/t27#11285) + assert model_ok("minimax/minimax-m3.1-flash:free"); + assert model_ok("nvidia/nemotron-3.5-lightning:free"); assert model_ok("-") == false; assert model_ok("") == false; assert model_ok("a b") == false; @@ -627,6 +632,10 @@ test resume_and_relaunch_keep_the_model { n = build_command(a, R_RELAUNCH, "abc-1", "opus[1m]", out[0..160]); assert n == 25; assert out[15] == 39 and out[20] == 91; + // claude --resume abc-1 --model 'minimax/minimax-m3.1-flash:free': a gateway's id keeps its slash and colon + n = build_command(a, R_RESUME, "abc-1", "minimax/minimax-m3.1-flash:free", out[0..160]); + assert n == 63; + assert out[30] == 39 and out[38] == 47 and out[57] == 58 and out[62] == 39; // a model that fails model_ok is left off assert build_command(a, R_RESUME, "abc-1", "a b", out[0..160]) == 21; assert build_command(a, R_RESUME, "abc-1", "-", out[0..160]) == 21; From fe7f34b590ea6688f2e0ad77708a4c1a329628e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 16:28:51 +0700 Subject: [PATCH 2/2] T27 OS keys: an OpenRouter free tier after the z.ai flash tier, paid last; the KEYS-tab request line follows in #11289 (Closes #11285) Merge of master plus sealing, gates run on the merged tree with t27c from master: tests clean with vacuous passes 0, seal --verify MATCH, tri types ratchet CLEAN, dupe_scan failure set a subset of master's. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/t27os_T27osKeys.json | 18 +++++------ .trinity/seals/t27os_T27osPtyLogic.json | 20 ++++++------- .trinity/seals/t27os_T27osSessionStore.json | 33 +++++++++++++++++++++ 3 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 .trinity/seals/t27os_T27osSessionStore.json diff --git a/.trinity/seals/t27os_T27osKeys.json b/.trinity/seals/t27os_T27osKeys.json index 9a3b0dad56..7c73d5903e 100644 --- a/.trinity/seals/t27os_T27osKeys.json +++ b/.trinity/seals/t27os_T27osKeys.json @@ -11,27 +11,27 @@ }, { "spec": "specs/t27os/session_store.t27", - "spec_hash": "sha256:7d05d7dda14587458a77291c3d587e3f08516d8e678f0a4bd06dcaab7f6d1f2f" + "spec_hash": "sha256:a2cb4e21d67129c1180a411b3e987116a279b17075ad22cf111c1892d292f178" } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:b423ffe1f6419b847847470552e43a3ec33ebee2d84e96e81d4e87064729ac1f", - "gen_hash_rust": "sha256:215fefc28d29127a94fbb24ae3dc56cc7a9ca1e44e97ee4dca1132fbfc1c296e", - "gen_hash_verilog": "sha256:0a213597aed4550a80b99d34c22367b58af23c1221e58a8e67dd83f768de08bf", - "gen_hash_zig": "sha256:8057d21cc4de8ffc9989b4f5848fc72a155f337939c9bb7671590c488d316679", + "gen_hash_c": "sha256:449de8d2408f2cdff9e62a3be15a35575e03e970b3ba7939d6e2cf3668af1e5d", + "gen_hash_rust": "sha256:908212d0d6510d83871d3893bbac1496d7d6cb41361419339efc3c9f25c73e45", + "gen_hash_verilog": "sha256:44fd4c25080a84a490d6dd5f1ecaac37e6648f59fa5abb133d314e21a499dbee", + "gen_hash_zig": "sha256:d24916d8dcdbf6eb0b6eecd5745ea6912a6ea382c87ec320db2f74be596778a1", "module": "T27osKeys", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-20T09:05:17Z", + "sealed_at": "2026-10-20T09:28:17Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:3e51f65c621494d2e4ef87f89b592ed350831804a46822684f624db7891b9577", + "spec_hash": "sha256:0ac09b2a849736c10bad4c68b726ef7b8f6bf899df91380b0aa0117b534cad8d", "spec_path": "specs/t27os/keys.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 86, - "total": 86, + "passed": 91, + "total": 91, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/t27os_T27osPtyLogic.json b/.trinity/seals/t27os_T27osPtyLogic.json index 06b37d4a2c..59d829a4d5 100644 --- a/.trinity/seals/t27os_T27osPtyLogic.json +++ b/.trinity/seals/t27os_T27osPtyLogic.json @@ -11,7 +11,7 @@ }, { "spec": "specs/t27os/keys.t27", - "spec_hash": "sha256:3e51f65c621494d2e4ef87f89b592ed350831804a46822684f624db7891b9577" + "spec_hash": "sha256:0ac09b2a849736c10bad4c68b726ef7b8f6bf899df91380b0aa0117b534cad8d" }, { "spec": "specs/t27os/pane_state.t27", @@ -23,27 +23,27 @@ }, { "spec": "specs/t27os/session_store.t27", - "spec_hash": "sha256:7d05d7dda14587458a77291c3d587e3f08516d8e678f0a4bd06dcaab7f6d1f2f" + "spec_hash": "sha256:a2cb4e21d67129c1180a411b3e987116a279b17075ad22cf111c1892d292f178" } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:08b4f5a76814d98a09ecf6ca924ac7d47ec59c52950c40e7af7dfe6faeaa7b92", - "gen_hash_rust": "sha256:446e88354984b34b59cc6166aa86d5ae52e7b6f83d72277d7a1279458a6f439b", - "gen_hash_verilog": "sha256:f63ab08ec6b9dc43336c69a4a8bb6ddd103039433c5815914b55ee7fe17a412d", - "gen_hash_zig": "sha256:17f6f770abf17c72f25b1c103e6d6f4235ea228db300cfce8ebf5db6c352b742", + "gen_hash_c": "sha256:74cc35dfad9496e6cf61f952e3a4e9a676bdd2e180998adc36fb840b4639067e", + "gen_hash_rust": "sha256:f3972f4a7a3a6483ba1311954cafb3c4dd40212e3cc4bb2303581f20746a6bce", + "gen_hash_verilog": "sha256:193ea5bf25afd43f786f524f5949d3d50d2f9cc5f54e74f8b21782b6078f0b5d", + "gen_hash_zig": "sha256:64258e57817aaddf5151d600352a96b7a6af21486de196d508029a18f61a9049", "module": "T27osPtyLogic", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-20T09:05:23Z", + "sealed_at": "2026-10-20T09:28:23Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:c2559b3d076400e0c401bb0bf8ea44eaef9b713ce57a8400e8d0c610dfb6d355", + "spec_hash": "sha256:538e4fc3ac08006b5b483666065ed32ceb4602577a548abed98b9b0110876052", "spec_path": "specs/t27os/pty_logic.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 45, - "total": 45, + "passed": 48, + "total": 48, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/t27os_T27osSessionStore.json b/.trinity/seals/t27os_T27osSessionStore.json new file mode 100644 index 0000000000..a09c30289e --- /dev/null +++ b/.trinity/seals/t27os_T27osSessionStore.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+2d3203e90", + "closure": [ + { + "spec": "specs/t27os/agents.t27", + "spec_hash": "sha256:c4b89c1bd1dcb751fada2ae1f0330e250d17c5085ea73acdc38fac13548277ee" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:4719bc4428183be5e1b7f776166ed3d51beb257f01a203dff8c645aabc9f7dbb", + "gen_hash_rust": "sha256:b59ed7ea11082a9dc36d2066a125c59084318e6297558143728f7bfac1b22fcc", + "gen_hash_verilog": "sha256:574e0f7acf428f390410960dc4cc69eb7d62c470d70dc17319404962dc8726b2", + "gen_hash_zig": "sha256:bf71c5afacc3182bfea2ea55efd16f395a402b78a4c2a522430f88972bdf7878", + "module": "T27osSessionStore", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-20T09:28:28Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:a2cb4e21d67129c1180a411b3e987116a279b17075ad22cf111c1892d292f178", + "spec_path": "specs/t27os/session_store.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 16, + "total": 16, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file