Skip to content
96 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
2 * Which model a reply runs on. Pure, so it is tested on its own.
3 *
4 * Nobody picks a model: g1t routes each step to the tier it needs (chat
5 * replies start on `small`), and the agent's definition only limits that
6 * (docs/WORKSPACE.md, "Model routing"). The model behind each tier is the
7 * runner's routing policy (`AGENT_ROUTING`, with staff's defaults on top),
8 * read through the runner's own module so the two never disagree.
9 */
10import type { AgentRouting as AgentLimits, ModelTier } from "@g1t/contracts";
11
12import type { AgentRouting as Policy, TokenPrice } from "../../runner/src/model-env.ts";
13
14const ORDER: ModelTier[] = ["small", "large", "frontier"];
15
16/** Where a chat reply starts before the agent's limits apply. */
17export const REPLY_TIER: ModelTier = "small";
18
19export function isTier(value: unknown): value is ModelTier {
20 return value === "small" || value === "large" || value === "frontier";
21}
22
23/**
24 * `tier` held between the agent's floor and ceiling. When a definition
25 * has the floor above the ceiling (saved before validation said no, or
26 * edited by hand), the ceiling wins: it is the spending rail, and a rail
27 * is never crossed to honour a preference.
28 */
29export function clampTier(tier: ModelTier, floor: ModelTier | null, ceiling: ModelTier | null): ModelTier {
30 let at = ORDER.indexOf(tier);
31 if (floor && ORDER.indexOf(floor) > at) at = ORDER.indexOf(floor);
32 if (ceiling && ORDER.indexOf(ceiling) < at) at = ORDER.indexOf(ceiling);
33 return ORDER[at];
34}
35
36/** Whether a floor and a ceiling can both hold. */
37export function limitsAgree(floor: ModelTier | null, ceiling: ModelTier | null): boolean {
38 return !floor || !ceiling || ORDER.indexOf(floor) <= ORDER.indexOf(ceiling);
39}
40
41/** The word the agent's `providers` list uses for g1t's hosted models. */
42export const HOSTED = "g1t";
43/** The word the agent's `providers` list uses for the workspace's own providers, whichever they are. */
44export const OWN = "workspace";
45
46/**
47 * Where an agent's model calls may go: g1t's hosted models, the
48 * workspace's own provider, or both. `providers` empty means whatever the
49 * workspace allows; `g1t` is g1t's hosted models, `workspace` any of the
50 * workspace's own providers, and anything else one of them by integration
51 * id. `ownId` is the workspace's own model connection, if it has one.
52 */
53export function allowedProviders(limits: Pick<AgentLimits, "providers">, ownId: string | null): { hosted: boolean; own: boolean } {
54 const list = (limits.providers ?? []).map((p) => p.trim()).filter(Boolean);
55 if (!list.length) return { hosted: true, own: ownId != null };
56 return { hosted: list.includes(HOSTED), own: ownId != null && (list.includes(OWN) || list.includes(ownId)) };
57}
58
59/** The model a pinned `provider/model` names, or null. */
60export function pinnedModel(pinned: string | null | undefined): string | null {
61 if (!pinned) return null;
62 const at = pinned.indexOf("/");
63 const model = (at >= 0 ? pinned.slice(at + 1) : pinned).trim();
64 return model || null;
65}
66
67export type ReplyModel = {
68 tier: ModelTier;
69 /** Sent to the provider. */
70 model: string;
71 /** Shown to people and on the bill. */
72 modelName: string;
73 /** The provider's list price, when g1t knows it. */
74 price: TokenPrice | null;
75};
76
77/**
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)78 * The model a reply runs on: the reply tier (or `start`, where the work
79 * calls for another, or the tier the workspace chose for replies), held to
80 * the agent's limits, and the model the policy
Chat and workspace agents: channels, DMs and named agents you talk to81 * puts behind it. A pinned model, or a workspace route that names its own
82 * model, replaces the tier's model; it is not priced here, since it runs on
83 * the workspace's provider.
84 */
85export function replyModel(
86 policy: Pick<Policy, "tiers">,
87 limits: Pick<AgentLimits, "floor" | "ceiling" | "pinned">,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)88 options: { chosen?: ModelTier | null; named?: string | null; start?: ModelTier } = {},
Chat and workspace agents: channels, DMs and named agents you talk to89): ReplyModel {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)90 const start = options.chosen && isTier(options.chosen) ? options.chosen : (options.start ?? REPLY_TIER);
Chat and workspace agents: channels, DMs and named agents you talk to91 const tier = clampTier(start, limits.floor, limits.ceiling);
92 const named = options.named || pinnedModel(limits.pinned);
93 if (named) return { tier, model: named, modelName: named, price: null };
94 const route = policy.tiers[tier];
95 return { tier, model: route.model, modelName: route.modelName, price: route.price ?? null };
96}

This file's history is long; its oldest lines are credited to the oldest commit read.