@anchrd/intel-ui 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentModel } from "@anchrd/intel-contract";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import type { ModelFacts, ModelPrice } from "./agent-models.types.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Which models this installation offers.
|
|
@@ -7,8 +8,7 @@ import { z } from "zod";
|
|
|
7
8
|
* ⚠️ There is no endpoint to ask. The runtime's registry answers one question — "is this provider
|
|
8
9
|
* configured" — and only when a run is already starting; nothing anywhere lists what a deployment
|
|
9
10
|
* pays for. Rather than invent an API for one screen, the list is build configuration
|
|
10
|
-
* (`VITE_AGENT_MODELS`, a JSON array of `{provider, model}`), with the
|
|
11
|
-
* fixtures use as the default.
|
|
11
|
+
* (`VITE_AGENT_MODELS`, a JSON array of `{provider, model}`), with the offering below as the default.
|
|
12
12
|
*
|
|
13
13
|
* ⚠️ The definition's current model is always offered, even when it is not in the list. A select
|
|
14
14
|
* that silently dropped it would turn "look at this agent" into "change this agent" for anyone who
|
|
@@ -16,10 +16,105 @@ import { z } from "zod";
|
|
|
16
16
|
*/
|
|
17
17
|
const ConfiguredModels = z.array(AgentModel);
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Context window and price per model, collected 2026-08-06 from
|
|
21
|
+
* `GET /accounts/{id}/ai/models/search` (Workers AI, every entry carrying `function_calling`) and
|
|
22
|
+
* from Anthropic's model list. Both figures are shown beside the select, so this table is the one
|
|
23
|
+
* place to correct when they move.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ It is a copy and it ages silently. Cloudflare changes context windows and prices without
|
|
26
|
+
* notice, and a wrong price in the interface is worse than none — re-read the endpoint above rather
|
|
27
|
+
* than editing a number from memory. Asking live from the browser is not the way out: the account
|
|
28
|
+
* token does not belong in a SPA.
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ Workers AI models that do not carry `function_calling` are deliberately absent. An agent is a
|
|
31
|
+
* tool loop; a model that cannot call a tool cannot run one, and offering it would only produce a
|
|
32
|
+
* broken agent for whoever picked it.
|
|
33
|
+
*/
|
|
34
|
+
const catalog: Record<string, { name: string; contextTokens: number; price: ModelPrice }> = {
|
|
35
|
+
"anthropic:claude-opus-5": price("Opus 5", 1_000_000, 5, 25),
|
|
36
|
+
"anthropic:claude-sonnet-5": price("Sonnet 5", 1_000_000, 3, 15),
|
|
37
|
+
"anthropic:claude-sonnet-4": price("Sonnet 4", 200_000, 3, 15),
|
|
38
|
+
"anthropic:claude-haiku-4-5": price("Haiku 4.5", 200_000, 1, 5),
|
|
39
|
+
"workers-ai:@cf/openai/gpt-oss-120b": price("GPT-OSS 120B", 128_000, 0.35, 0.75),
|
|
40
|
+
"workers-ai:@cf/openai/gpt-oss-20b": price("GPT-OSS 20B", 128_000, 0.2, 0.3),
|
|
41
|
+
"workers-ai:@cf/moonshotai/kimi-k2.6": price("Kimi K2.6", 262_144, 0.95, 4),
|
|
42
|
+
"workers-ai:@cf/moonshotai/kimi-k2.7-code": price("Kimi K2.7 Code", 262_144, 0.95, 4),
|
|
43
|
+
"workers-ai:@cf/zai-org/glm-5.2": price("GLM 5.2", 262_144, 1.4, 4.4),
|
|
44
|
+
"workers-ai:@cf/zai-org/glm-4.7-flash": price("GLM 4.7 Flash", 131_072, 0.0605, 0.4),
|
|
45
|
+
"workers-ai:@cf/google/gemma-4-26b-a4b-it": price("Gemma 4 26B", 256_000, 0.1, 0.3),
|
|
46
|
+
"workers-ai:@cf/nvidia/nemotron-3-120b-a12b": price("Nemotron 3 120B", 256_000, 0.5, 1.5),
|
|
47
|
+
"workers-ai:@cf/meta/llama-4-scout-17b-16e-instruct": price("Llama 4 Scout", 131_000, 0.27, 0.85),
|
|
48
|
+
"workers-ai:@cf/meta/llama-3.3-70b-instruct-fp8-fast": price(
|
|
49
|
+
"Llama 3.3 70B Fast",
|
|
50
|
+
24_000,
|
|
51
|
+
0.293,
|
|
52
|
+
2.253,
|
|
53
|
+
),
|
|
54
|
+
"workers-ai:@cf/mistralai/mistral-small-3.1-24b-instruct": price(
|
|
55
|
+
"Small 3.1 24B",
|
|
56
|
+
128_000,
|
|
57
|
+
0.351,
|
|
58
|
+
0.555,
|
|
59
|
+
),
|
|
60
|
+
"workers-ai:@cf/ibm-granite/granite-4.0-h-micro": price(
|
|
61
|
+
"Granite 4.0 Micro",
|
|
62
|
+
131_000,
|
|
63
|
+
0.017,
|
|
64
|
+
0.112,
|
|
65
|
+
),
|
|
66
|
+
"workers-ai:@cf/qwen/qwen3-30b-a3b-fp8": price("Qwen3 30B", 32_768, 0.0509, 0.335),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function price(
|
|
70
|
+
name: string,
|
|
71
|
+
contextTokens: number,
|
|
72
|
+
inputPerMillion: number,
|
|
73
|
+
outputPerMillion: number,
|
|
74
|
+
): { name: string; contextTokens: number; price: ModelPrice } {
|
|
75
|
+
return { name, contextTokens, price: { inputPerMillion, outputPerMillion } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ⚠️ The brand comes from the model id, never from `provider`. `provider` says who serves the model,
|
|
80
|
+
* so every Workers AI entry would read "Cloudflare" — thirteen identical labels in a list whose whole
|
|
81
|
+
* job is to tell them apart. The name a reader looks for sits in the id, as its second segment:
|
|
82
|
+
* `@cf/openai/gpt-oss-120b` is OpenAI's.
|
|
83
|
+
*/
|
|
84
|
+
const brands: Record<string, string> = {
|
|
85
|
+
aisingapore: "AI Singapore",
|
|
86
|
+
"deepseek-ai": "DeepSeek",
|
|
87
|
+
google: "Google",
|
|
88
|
+
"ibm-granite": "IBM",
|
|
89
|
+
meta: "Meta",
|
|
90
|
+
"meta-llama": "Meta",
|
|
91
|
+
mistralai: "Mistral",
|
|
92
|
+
moonshotai: "Moonshot",
|
|
93
|
+
nvidia: "NVIDIA",
|
|
94
|
+
openai: "OpenAI",
|
|
95
|
+
qwen: "Qwen",
|
|
96
|
+
"zai-org": "Z.ai",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const firstFallback: AgentModel = { provider: "anthropic", model: "claude-sonnet-5" };
|
|
20
100
|
const fallback: [AgentModel, ...AgentModel[]] = [
|
|
21
101
|
firstFallback,
|
|
102
|
+
{ provider: "anthropic", model: "claude-opus-5" },
|
|
103
|
+
{ provider: "anthropic", model: "claude-haiku-4-5" },
|
|
104
|
+
{ provider: "anthropic", model: "claude-sonnet-4" },
|
|
105
|
+
{ provider: "workers-ai", model: "@cf/openai/gpt-oss-120b" },
|
|
106
|
+
{ provider: "workers-ai", model: "@cf/openai/gpt-oss-20b" },
|
|
107
|
+
{ provider: "workers-ai", model: "@cf/moonshotai/kimi-k2.6" },
|
|
108
|
+
{ provider: "workers-ai", model: "@cf/moonshotai/kimi-k2.7-code" },
|
|
109
|
+
{ provider: "workers-ai", model: "@cf/zai-org/glm-5.2" },
|
|
110
|
+
{ provider: "workers-ai", model: "@cf/zai-org/glm-4.7-flash" },
|
|
111
|
+
{ provider: "workers-ai", model: "@cf/google/gemma-4-26b-a4b-it" },
|
|
112
|
+
{ provider: "workers-ai", model: "@cf/nvidia/nemotron-3-120b-a12b" },
|
|
113
|
+
{ provider: "workers-ai", model: "@cf/meta/llama-4-scout-17b-16e-instruct" },
|
|
22
114
|
{ provider: "workers-ai", model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast" },
|
|
115
|
+
{ provider: "workers-ai", model: "@cf/mistralai/mistral-small-3.1-24b-instruct" },
|
|
116
|
+
{ provider: "workers-ai", model: "@cf/ibm-granite/granite-4.0-h-micro" },
|
|
117
|
+
{ provider: "workers-ai", model: "@cf/qwen/qwen3-30b-a3b-fp8" },
|
|
23
118
|
];
|
|
24
119
|
|
|
25
120
|
export function modelKey(model: AgentModel): string {
|
|
@@ -36,6 +131,50 @@ export function parseModelKey(key: string): AgentModel | null {
|
|
|
36
131
|
return parsed.success ? parsed.data : null;
|
|
37
132
|
}
|
|
38
133
|
|
|
134
|
+
// How a model is written for a person: brand first, then the model's own name, then the two numbers
|
|
135
|
+
// somebody actually chooses on. A model outside the catalog keeps its own id as the name and reports
|
|
136
|
+
// no figures at all.
|
|
137
|
+
export function modelFacts(model: AgentModel): ModelFacts {
|
|
138
|
+
const known = catalog[modelKey(model)];
|
|
139
|
+
if (known) {
|
|
140
|
+
return {
|
|
141
|
+
brand: brandOf(model),
|
|
142
|
+
name: known.name,
|
|
143
|
+
contextTokens: known.contextTokens,
|
|
144
|
+
price: known.price,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { brand: brandOf(model), name: unknownName(model), contextTokens: null, price: null };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function brandOf(model: AgentModel): string {
|
|
151
|
+
if (model.provider === "anthropic") return "Claude";
|
|
152
|
+
const vendor = model.model.split("/")[1];
|
|
153
|
+
if (!vendor) return "Workers AI";
|
|
154
|
+
return brands[vendor] ?? vendor;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The id itself, not a prettified guess. "claude-opus-4-1" would become "Opus 4 1" under any rule
|
|
158
|
+
// simple enough to write here, and a wrong name is harder to recognise than a raw one.
|
|
159
|
+
function unknownName(model: AgentModel): string {
|
|
160
|
+
if (model.provider === "anthropic") return model.model.replace(/^claude-/, "");
|
|
161
|
+
const segments = model.model.split("/");
|
|
162
|
+
return segments[segments.length - 1] ?? model.model;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function formatContextTokens(tokens: number): string {
|
|
166
|
+
if (tokens >= 1_000_000) return `${Math.round(tokens / 1_000_000)}M`;
|
|
167
|
+
if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`;
|
|
168
|
+
return String(tokens);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ⚠️ Two decimals at least, three at most, and always per one million tokens. Granite costs $0.017
|
|
172
|
+
// per million in: rounded to two decimals it reads $0.02, which is the same price as a model that
|
|
173
|
+
// costs seventeen percent more.
|
|
174
|
+
export function formatPricePerMillion(dollars: number, locale: string): string {
|
|
175
|
+
return dollars.toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 3 });
|
|
176
|
+
}
|
|
177
|
+
|
|
39
178
|
export function configuredModels(): AgentModel[] {
|
|
40
179
|
const raw = import.meta.env.VITE_AGENT_MODELS;
|
|
41
180
|
if (!raw) return fallback;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** What a reader needs in order to pick a model: whose it is, what it is called, and what it costs. */
|
|
2
|
+
export interface ModelFacts {
|
|
3
|
+
brand: string;
|
|
4
|
+
name: string;
|
|
5
|
+
/**
|
|
6
|
+
* ⚠️ Both are null for a model this installation has no figures for, and the display then shows
|
|
7
|
+
* nothing rather than a zero. A definition is written by MCP and by other installations too, so an
|
|
8
|
+
* unknown model is normal — and an invented "0" would be a false statement about money.
|
|
9
|
+
*/
|
|
10
|
+
contextTokens: number | null;
|
|
11
|
+
price: ModelPrice | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Always US dollars per one million tokens — never per 1k, never a single blended number. */
|
|
15
|
+
export interface ModelPrice {
|
|
16
|
+
inputPerMillion: number;
|
|
17
|
+
outputPerMillion: number;
|
|
18
|
+
}
|
|
@@ -10,7 +10,15 @@ import { Check, Copy, Info, KeyRound, MessageSquare, Plus, Trash2, Wrench } from
|
|
|
10
10
|
import { useState } from "react";
|
|
11
11
|
import { type AgentDefinitionHandle, asDraft } from "@/agent/agent-definition/agent-definition.ts";
|
|
12
12
|
import { useEntryTitle } from "@/agent/agent-entry-title/agent-entry-title.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
formatContextTokens,
|
|
15
|
+
formatPricePerMillion,
|
|
16
|
+
modelFacts,
|
|
17
|
+
modelKey,
|
|
18
|
+
parseModelKey,
|
|
19
|
+
selectableModels,
|
|
20
|
+
} from "@/agent/agent-models/agent-models.ts";
|
|
21
|
+
import type { ModelFacts } from "@/agent/agent-models/agent-models.types.ts";
|
|
14
22
|
import {
|
|
15
23
|
Select,
|
|
16
24
|
SelectContent,
|
|
@@ -699,28 +707,69 @@ function ModelSection({
|
|
|
699
707
|
}) {
|
|
700
708
|
const { i18n } = useIntelRouterContext();
|
|
701
709
|
const models = selectableModels(definition.model);
|
|
710
|
+
const current = modelFacts(definition.model);
|
|
702
711
|
|
|
703
712
|
return (
|
|
704
713
|
<Section title={i18n.t("agent.model")} hint={i18n.t("agent.modelHint")}>
|
|
705
|
-
<
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
<
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
714
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
|
715
|
+
<Select
|
|
716
|
+
value={modelKey(definition.model)}
|
|
717
|
+
onValueChange={(key) => {
|
|
718
|
+
const model = parseModelKey(key);
|
|
719
|
+
if (model) agent.save.mutate({ ...asDraft(definition), model });
|
|
720
|
+
}}
|
|
721
|
+
>
|
|
722
|
+
<SelectTrigger aria-label={i18n.t("agent.model")} className="w-full max-w-md sm:w-72">
|
|
723
|
+
{/* ⚠️ Own children rather than the default mirror of the chosen item: the item carries
|
|
724
|
+
the figures too, and mirrored into the trigger they would stand twice in one row. */}
|
|
725
|
+
<SelectValue>
|
|
726
|
+
<ModelName facts={current} />
|
|
727
|
+
</SelectValue>
|
|
728
|
+
</SelectTrigger>
|
|
729
|
+
<SelectContent>
|
|
730
|
+
{models.map((model) => {
|
|
731
|
+
const facts = modelFacts(model);
|
|
732
|
+
return (
|
|
733
|
+
<SelectItem key={modelKey(model)} value={modelKey(model)}>
|
|
734
|
+
{/* ⚠️ The space is not formatting. The option's accessible name is its text run
|
|
735
|
+
together, so without it a screen reader reads "…70B Fast24k context". The eye
|
|
736
|
+
never notices, because the row is a flex box with a gap. */}
|
|
737
|
+
<ModelName facts={facts} /> <ModelFigures facts={facts} />
|
|
738
|
+
</SelectItem>
|
|
739
|
+
);
|
|
740
|
+
})}
|
|
741
|
+
</SelectContent>
|
|
742
|
+
</Select>
|
|
743
|
+
<ModelFigures facts={current} />
|
|
744
|
+
</div>
|
|
724
745
|
</Section>
|
|
725
746
|
);
|
|
726
747
|
}
|
|
748
|
+
|
|
749
|
+
// Brand first, then the model's own name — "Claude Sonnet 5", not "claude-sonnet-5 · anthropic". The
|
|
750
|
+
// provider stays in the definition; it names who serves the model, which is not what anybody reads a
|
|
751
|
+
// list of models for.
|
|
752
|
+
function ModelName({ facts }: { facts: ModelFacts }) {
|
|
753
|
+
return (
|
|
754
|
+
<span className="truncate">
|
|
755
|
+
<span className="font-medium">{facts.brand}</span> {facts.name}
|
|
756
|
+
</span>
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// The two numbers a model is actually chosen on. Absent for a model this installation has no figures
|
|
761
|
+
// for — the row then simply ends, because a made-up zero would be a false statement about money.
|
|
762
|
+
function ModelFigures({ facts }: { facts: ModelFacts }) {
|
|
763
|
+
const { i18n } = useIntelRouterContext();
|
|
764
|
+
if (facts.contextTokens === null || facts.price === null) return null;
|
|
765
|
+
|
|
766
|
+
return (
|
|
767
|
+
<span className="text-xs whitespace-nowrap text-muted-foreground">
|
|
768
|
+
{i18n.t("agent.modelFigures", {
|
|
769
|
+
context: formatContextTokens(facts.contextTokens),
|
|
770
|
+
input: formatPricePerMillion(facts.price.inputPerMillion, i18n.locale),
|
|
771
|
+
output: formatPricePerMillion(facts.price.outputPerMillion, i18n.locale),
|
|
772
|
+
})}
|
|
773
|
+
</span>
|
|
774
|
+
);
|
|
775
|
+
}
|
package/src/i18n/en.json
CHANGED
|
@@ -347,6 +347,7 @@
|
|
|
347
347
|
"agent.cronHint": "Five fields, in UTC: minute, hour, day of month, month, day of week. “0 8 * * *” is every day at 08:00.",
|
|
348
348
|
"agent.model": "Model",
|
|
349
349
|
"agent.modelHint": "Which model does the thinking. A change is picked up by the runtime within a minute.",
|
|
350
|
+
"agent.modelFigures": "{context} context · ${input} in / ${output} out per 1M tokens",
|
|
350
351
|
"agent.calendar": "Upcoming runs",
|
|
351
352
|
"agent.calendarUtc": "All times are UTC, because that is when the agent runs.",
|
|
352
353
|
"agent.calendarUnreadable": "None of this agent's schedules can be read as a cron expression, so nothing is planned.",
|