@dan-ai-studio/dshopencodego 0.1.14 → 0.1.16

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.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The provider's own Go documentation as a data source.
3
+ *
4
+ * No API exposes the per-model allowances: the gateway's `/models` answers ids,
5
+ * and `/usage` answers account-wide percentages. The numbers live on a docs page
6
+ * whose source is a markdown file in the provider's repository, so this module
7
+ * fetches that file and parses its tables — allowances (with the provider's own
8
+ * request estimates) and the endpoint table that names each model's protocol.
9
+ *
10
+ * The document is the primary source. The transcribed table in `go-limits.ts`
11
+ * stays behind it as a startup seed and a permanent fallback for the day the
12
+ * URL moves, and its age is surfaced wherever it is used.
13
+ *
14
+ * @module @dan-ai-studio/dshopencodego/catalog/go-doc
15
+ */
16
+ import { attributionHeaders, LlmError } from '@deepseek-ai/dsh-llm';
17
+ import { METADATA_FETCH_TIMEOUT_MS } from "./constants.js";
18
+ import { readBoundedText } from "./json-response.js";
19
+ /**
20
+ * Documentation sources, tried in order.
21
+ *
22
+ * The repository file is the source of truth; the jsDelivr entry caches the
23
+ * same path and is tried when GitHub itself is unreachable.
24
+ */
25
+ export const GO_DOC_URLS = [
26
+ 'https://raw.githubusercontent.com/anomalyco/opencode/dev/packages/web/src/content/docs/go.mdx',
27
+ 'https://cdn.jsdelivr.net/gh/anomalyco/opencode@dev/packages/web/src/content/docs/go.mdx',
28
+ ];
29
+ /** The document is about 30 KB; the cap leaves room without inviting a flood. */
30
+ export const GO_DOC_MAX_BYTES = 1024 * 1024;
31
+ /** The protocol each documented endpoint path names. */
32
+ const ENDPOINT_PROTOCOLS = {
33
+ '/chat/completions': 'openai-completions',
34
+ '/messages': 'anthropic-messages',
35
+ '/responses': 'openai-responses',
36
+ };
37
+ /** Strip markdown decoration (`**bold**`, `` `code` ``) from one cell. */
38
+ function cell(value) {
39
+ return (value ?? '').replace(/[*`]/g, '').trim();
40
+ }
41
+ /**
42
+ * A display name without its tier suffix, so the three tables join.
43
+ * `Qwen3.7 Plus (≤ 256K tokens)` and `DeepSeek V4 Pro (Peak)` both reduce to the
44
+ * name the endpoint table carries.
45
+ */
46
+ function baseName(name) {
47
+ return name.replace(/\s*\(.*\)\s*$/, '').trim();
48
+ }
49
+ /** `$60` → 60; `Unlimited 限时` → `unlimited`; anything else → undefined. */
50
+ function parseAllowance(value) {
51
+ if (/^unlimited\b/i.test(value) || value.startsWith('无限制'))
52
+ return 'unlimited';
53
+ const match = value.match(/\$([\d,]+(?:\.\d+)?)/);
54
+ if (match?.[1] === undefined)
55
+ return undefined;
56
+ const amount = Number(match[1].replace(/,/g, ''));
57
+ return Number.isFinite(amount) ? amount : undefined;
58
+ }
59
+ /** `6,320` → 6320; `Unlimited` → `unlimited`; anything else → undefined. */
60
+ function parseCount(value) {
61
+ if (/^unlimited\b/i.test(value) || value.startsWith('无限制'))
62
+ return 'unlimited';
63
+ const amount = Number(value.replace(/,/g, ''));
64
+ return value.length > 0 && Number.isFinite(amount) ? amount : undefined;
65
+ }
66
+ /**
67
+ * The protocol a documented endpoint names.
68
+ *
69
+ * Matched by path suffix: the table spells full URLs, and every supported API
70
+ * lives at a distinct tail (`/chat/completions`, `/messages`, `/responses`).
71
+ */
72
+ function protocolOfEndpoint(endpoint) {
73
+ for (const [suffix, protocol] of Object.entries(ENDPOINT_PROTOCOLS)) {
74
+ if (endpoint.endsWith(suffix))
75
+ return protocol;
76
+ }
77
+ return undefined;
78
+ }
79
+ /**
80
+ * The data rows of the table whose header carries `required`.
81
+ *
82
+ * Data starts after the header and its separator, and ends at the first line
83
+ * that is not a table row, so three tables in one document stay apart.
84
+ * @param lines - the document, split into lines.
85
+ * @param required - a header cell that table alone carries.
86
+ * @returns one array of trimmed cells per data row.
87
+ */
88
+ function tableRows(lines, required) {
89
+ const start = lines.findIndex(line => line.startsWith('|') && line.includes(required));
90
+ if (start < 0)
91
+ return [];
92
+ const rows = [];
93
+ for (let index = start + 2; index < lines.length; index++) {
94
+ const line = lines[index] ?? '';
95
+ if (!line.startsWith('|'))
96
+ break;
97
+ rows.push(line.split('|').slice(1, -1).map(entry => entry.trim()));
98
+ }
99
+ return rows;
100
+ }
101
+ /**
102
+ * Parse the Go documentation into the two data sets this plugin consumes.
103
+ *
104
+ * Titles are the join key: the allowance tables name models the way the page
105
+ * presents them while the endpoint table carries the ids, so ids come from the
106
+ * endpoint table and the other two attach by normalised display name. A row
107
+ * that names no known model, or states no usable number, is skipped rather than
108
+ * guessed; a tiered model whose rows disagree is dropped entirely.
109
+ * @param markdown - the raw document text.
110
+ * @returns the parsed allowances and protocols; both may be empty when the
111
+ * document does not carry the expected tables.
112
+ */
113
+ export function parseGoDocument(markdown) {
114
+ const lines = markdown.split('\n');
115
+ const ids = new Map();
116
+ const protocols = new Map();
117
+ for (const row of tableRows(lines, 'Model ID')) {
118
+ const name = cell(row[0]);
119
+ const id = cell(row[1]);
120
+ if (name.length === 0 || id.length === 0)
121
+ continue;
122
+ ids.set(name, id);
123
+ const protocol = protocolOfEndpoint(cell(row[2]));
124
+ if (protocol !== undefined)
125
+ protocols.set(id, protocol);
126
+ }
127
+ const quotas = new Map();
128
+ for (const row of tableRows(lines, 'Monthly limit')) {
129
+ const id = ids.get(baseName(cell(row[0])));
130
+ if (id === undefined)
131
+ continue;
132
+ const monthlyUsd = parseAllowance(cell(row[5]));
133
+ if (monthlyUsd === undefined)
134
+ continue;
135
+ const previous = quotas.get(id);
136
+ // Tiered rows (token bands, peak/off-peak) repeat one allowance; the first
137
+ // row wins, and a disagreement drops the model instead of picking a band.
138
+ if (previous === undefined)
139
+ quotas.set(id, { monthlyUsd });
140
+ else if (previous.monthlyUsd !== monthlyUsd)
141
+ quotas.delete(id);
142
+ }
143
+ for (const row of tableRows(lines, 'requests per month')) {
144
+ const id = ids.get(baseName(cell(row[0])));
145
+ if (id === undefined)
146
+ continue;
147
+ const monthlyRequests = parseCount(cell(row[3]));
148
+ const quota = quotas.get(id);
149
+ if (monthlyRequests === undefined || quota === undefined)
150
+ continue;
151
+ quotas.set(id, { ...quota, monthlyRequests });
152
+ }
153
+ return { quotas, protocols };
154
+ }
155
+ /**
156
+ * Fetch and parse the documentation, trying each source in turn.
157
+ *
158
+ * A source that answers without a usable allowance table counts as a failure,
159
+ * not as an empty document: that is what a moved or restructured page looks
160
+ * like, and the caller must keep its previous data rather than blank the page.
161
+ * @param signal - caller cancellation, if any.
162
+ * @returns the parsed document from the first source that carried one.
163
+ * @throws {LlmError} `DOCUMENT_UNAVAILABLE` when every source failed.
164
+ */
165
+ export async function fetchGoDocument(signal) {
166
+ const failures = [];
167
+ for (const url of GO_DOC_URLS) {
168
+ try {
169
+ const timeout = AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS);
170
+ const response = await fetch(url, {
171
+ redirect: 'error',
172
+ headers: { ...attributionHeaders(), accept: 'text/plain', 'cache-control': 'no-cache' },
173
+ signal: signal === undefined ? timeout : AbortSignal.any([signal, timeout]),
174
+ });
175
+ if (!response.ok) {
176
+ await response.body?.cancel().catch(() => { });
177
+ failures.push(`${url} answered HTTP ${response.status}`);
178
+ continue;
179
+ }
180
+ const document = parseGoDocument(await readBoundedText(response, GO_DOC_MAX_BYTES));
181
+ if (document.quotas.size === 0) {
182
+ failures.push(`${url} carried no allowance table`);
183
+ continue;
184
+ }
185
+ return document;
186
+ }
187
+ catch (error) {
188
+ failures.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
189
+ }
190
+ }
191
+ throw new LlmError(`could not read the Go documentation (${failures.join('; ')})`, 'DOCUMENT_UNAVAILABLE');
192
+ }
@@ -3,21 +3,26 @@
3
3
  *
4
4
  * Membership comes from the gateway listing, capability from models.dev, and
5
5
  * exact protocol/wire quirks from the installed pi-ai catalog, merged through
6
- * the ladder in `./protocol.ts`. The snapshot is cached for the configured
7
- * refresh interval, and an unknown model id forces one revalidation even inside
8
- * that interval — that is what makes a model the gateway added this morning
9
- * callable this afternoon without a plugin release.
6
+ * the ladder in `./protocol.ts`. Per-model allowances, and a protocol for the
7
+ * models nothing else covers, come from the provider's own documentation (see
8
+ * `./go-doc.ts`). The snapshot is cached for the configured refresh interval,
9
+ * and an unknown model id forces one revalidation even inside that interval —
10
+ * that is what makes a model the gateway added this morning callable this
11
+ * afternoon without a plugin release.
10
12
  *
11
13
  * Failures degrade rather than empty: a listing outage keeps the last known
12
14
  * models and marks the snapshot not-live, a metadata outage keeps the last
13
- * document, and a single unparseable model is reported by id while every other
14
- * model keeps serving.
15
+ * document, a documentation outage keeps the last parsed allowances (or leaves
16
+ * the page on its frozen seed), and a single unparseable model is reported by
17
+ * id while every other model keeps serving.
15
18
  *
16
19
  * @module @dan-ai-studio/dshopencodego/catalog
17
20
  */
18
21
  import type { Provider } from '@earendil-works/pi-ai';
19
22
  import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-llm';
23
+ import type { GoDoc } from './go-doc.ts';
20
24
  import type { ModelDefaults, ModelFacts } from './metadata.ts';
25
+ import type { GoQuota } from '../go-limits.ts';
21
26
  export { PROVIDER_ID, DISPLAY_NAME, DEFAULT_BASE_URL } from './constants.ts';
22
27
  export { readModelIds, fetchModelIds } from './gateway.ts';
23
28
  export { readOnlineMetadata, toPiModel, modelBaseURL } from './metadata.ts';
@@ -34,6 +39,12 @@ export interface CatalogSnapshot {
34
39
  readonly provider: Provider;
35
40
  /** Whether the gateway listing answered during this build. */
36
41
  readonly live: boolean;
42
+ /**
43
+ * Allowances parsed from the provider's own documentation, once a fetch has
44
+ * succeeded in this process. Absent until then, and across restarts; the page
45
+ * falls back to the frozen seed in `go-limits.ts`.
46
+ */
47
+ readonly documentQuotas?: ReadonlyMap<string, GoQuota>;
37
48
  /** Retained for explicit discovery when the listing failed. */
38
49
  readonly listingFailure?: unknown;
39
50
  readonly fetchedAtMs: number;
@@ -63,6 +74,11 @@ export interface CatalogOptions {
63
74
  /** Configured per-id protocol overrides. */
64
75
  readonly overrides: Readonly<Record<string, string>>;
65
76
  readonly observers?: CatalogObservers;
77
+ /**
78
+ * Read the provider documentation. Defaults to the real fetch; tests inject a
79
+ * fixed document so the suite stays offline.
80
+ */
81
+ readonly readDocument?: () => Promise<GoDoc>;
66
82
  }
67
83
  /**
68
84
  * One gateway's catalog. Requests reuse a cached snapshot; explicit discovery
@@ -75,6 +91,8 @@ export declare class OpencodeGoCatalog {
75
91
  private metadataDocument;
76
92
  private metadataETag;
77
93
  private lastMetadata;
94
+ /** The last parsed documentation; retained across failed fetches. */
95
+ private document;
78
96
  constructor(options: CatalogOptions);
79
97
  /** The cached snapshot, refreshed when it is older than the configured TTL. */
80
98
  snapshot(force?: boolean): Promise<CatalogSnapshot>;
@@ -89,10 +107,22 @@ export declare class OpencodeGoCatalog {
89
107
  * the id but this build cannot configure it, naming the reason.
90
108
  */
91
109
  forModel(id: string): Promise<CatalogSnapshot>;
92
- /** Revalidate both sources, reusing the metadata document when it is unchanged. */
110
+ /** Revalidate every source, reusing the metadata document when it is unchanged. */
93
111
  private build;
94
112
  /** Merge the listing with the metadata document and the installed entries. */
95
113
  private assemble;
114
+ /** The document allowances as a snapshot field, absent until one fetch lands. */
115
+ private documentQuotas;
116
+ /**
117
+ * Prefer the documented protocol over anything below the installed catalog.
118
+ *
119
+ * The endpoint table is the provider's own statement, so it outranks both the
120
+ * family guess and models.dev's SDK hint — this turns an `inferred` badge into
121
+ * a known answer for ids the installed catalog misses. It never overrides a
122
+ * builtin entry or a configured override: those carry the wire quirks the
123
+ * adapter relies on.
124
+ */
125
+ private withDocumentedProtocol;
96
126
  /** Facts for an id no source describes, from the ladder and the route defaults. */
97
127
  private inferredFacts;
98
128
  /** Conditional metadata GET: an unchanged document keeps its parsed form. */
@@ -3,15 +3,18 @@
3
3
  *
4
4
  * Membership comes from the gateway listing, capability from models.dev, and
5
5
  * exact protocol/wire quirks from the installed pi-ai catalog, merged through
6
- * the ladder in `./protocol.ts`. The snapshot is cached for the configured
7
- * refresh interval, and an unknown model id forces one revalidation even inside
8
- * that interval — that is what makes a model the gateway added this morning
9
- * callable this afternoon without a plugin release.
6
+ * the ladder in `./protocol.ts`. Per-model allowances, and a protocol for the
7
+ * models nothing else covers, come from the provider's own documentation (see
8
+ * `./go-doc.ts`). The snapshot is cached for the configured refresh interval,
9
+ * and an unknown model id forces one revalidation even inside that interval —
10
+ * that is what makes a model the gateway added this morning callable this
11
+ * afternoon without a plugin release.
10
12
  *
11
13
  * Failures degrade rather than empty: a listing outage keeps the last known
12
14
  * models and marks the snapshot not-live, a metadata outage keeps the last
13
- * document, and a single unparseable model is reported by id while every other
14
- * model keeps serving.
15
+ * document, a documentation outage keeps the last parsed allowances (or leaves
16
+ * the page on its frozen seed), and a single unparseable model is reported by
17
+ * id while every other model keeps serving.
15
18
  *
16
19
  * @module @dan-ai-studio/dshopencodego/catalog
17
20
  */
@@ -23,6 +26,7 @@ import { openAIResponsesApi } from '@earendil-works/pi-ai/api/openai-responses.l
23
26
  import { attributionHeaders, LlmError } from '@deepseek-ai/dsh-llm';
24
27
  import { DISPLAY_NAME, MODEL_METADATA_MAX_BYTES, MODEL_METADATA_URL, PROVIDER_ID, METADATA_FETCH_TIMEOUT_MS, } from "./constants.js";
25
28
  import { fetchModelIds } from "./gateway.js";
29
+ import { fetchGoDocument, GO_DOC_URLS } from "./go-doc.js";
26
30
  import { readBoundedJson } from "./json-response.js";
27
31
  import { readOnlineMetadata, toPiModel } from "./metadata.js";
28
32
  export { PROVIDER_ID, DISPLAY_NAME, DEFAULT_BASE_URL } from "./constants.js";
@@ -80,6 +84,8 @@ export class OpencodeGoCatalog {
80
84
  metadataDocument;
81
85
  metadataETag;
82
86
  lastMetadata;
87
+ /** The last parsed documentation; retained across failed fetches. */
88
+ document;
83
89
  constructor(options) {
84
90
  this.options = options;
85
91
  }
@@ -118,12 +124,14 @@ export class OpencodeGoCatalog {
118
124
  }
119
125
  return snapshot;
120
126
  }
121
- /** Revalidate both sources, reusing the metadata document when it is unchanged. */
127
+ /** Revalidate every source, reusing the metadata document when it is unchanged. */
122
128
  async build() {
123
129
  const builtin = builtinModels(this.options.baseURL);
124
- const [listing, metadata] = await Promise.allSettled([
130
+ const readDocument = this.options.readDocument ?? fetchGoDocument;
131
+ const [listing, metadata, documentation] = await Promise.allSettled([
125
132
  fetchModelIds(this.options.baseURL),
126
133
  this.refreshMetadata(builtin),
134
+ readDocument(),
127
135
  ]);
128
136
  if (metadata.status === 'rejected') {
129
137
  this.options.observers?.onFallback?.({
@@ -132,6 +140,18 @@ export class OpencodeGoCatalog {
132
140
  kept: this.lastMetadata?.models.size ?? 0,
133
141
  });
134
142
  }
143
+ if (documentation.status === 'fulfilled') {
144
+ this.document = documentation.value;
145
+ }
146
+ else {
147
+ // Not a page-stale event: the allowances keep their previous source, and
148
+ // the page says so wherever the frozen seed is in use.
149
+ this.options.observers?.onFallback?.({
150
+ url: GO_DOC_URLS[0] ?? 'the Go documentation',
151
+ error: documentation.reason,
152
+ kept: this.document?.quotas.size ?? 0,
153
+ });
154
+ }
135
155
  if (listing.status === 'rejected') {
136
156
  // Once observed, an outage must not resurrect retired models: the last
137
157
  // successful listing stays authoritative until a new one arrives.
@@ -149,6 +169,7 @@ export class OpencodeGoCatalog {
149
169
  live: false,
150
170
  listingFailure: listing.reason,
151
171
  fetchedAtMs: Date.now(),
172
+ ...this.documentQuotas(),
152
173
  };
153
174
  }
154
175
  // A metadata outage keeps the last successful parse: the previous document
@@ -177,7 +198,7 @@ export class OpencodeGoCatalog {
177
198
  // (`@ai-sdk/openai-compatible`) makes Chat Completions the best available
178
199
  // guess, and the route defaults size it; the settings surface marks it as
179
200
  // inferred so a wrong guess is visible and overridable.
180
- facts.set(id, this.inferredFacts(id, builtin));
201
+ facts.set(id, this.withDocumentedProtocol(this.inferredFacts(id, builtin)));
181
202
  }
182
203
  if (unavailable.size > 0) {
183
204
  this.options.observers?.onUnconfigured?.([...unavailable].map(([id, reason]) => ({ id, reason })));
@@ -188,8 +209,28 @@ export class OpencodeGoCatalog {
188
209
  provider: buildProvider(this.options.baseURL, [...facts.values()].map(fact => toPiModel(fact, this.options.baseURL))),
189
210
  live: true,
190
211
  fetchedAtMs: Date.now(),
212
+ ...this.documentQuotas(),
191
213
  };
192
214
  }
215
+ /** The document allowances as a snapshot field, absent until one fetch lands. */
216
+ documentQuotas() {
217
+ return this.document === undefined ? {} : { documentQuotas: this.document.quotas };
218
+ }
219
+ /**
220
+ * Prefer the documented protocol over anything below the installed catalog.
221
+ *
222
+ * The endpoint table is the provider's own statement, so it outranks both the
223
+ * family guess and models.dev's SDK hint — this turns an `inferred` badge into
224
+ * a known answer for ids the installed catalog misses. It never overrides a
225
+ * builtin entry or a configured override: those carry the wire quirks the
226
+ * adapter relies on.
227
+ */
228
+ withDocumentedProtocol(facts) {
229
+ if (facts.protocolSource !== 'inferred' && facts.protocolSource !== 'online')
230
+ return facts;
231
+ const documented = this.document?.protocols.get(facts.id);
232
+ return documented === undefined ? facts : { ...facts, api: documented, protocolSource: 'document' };
233
+ }
193
234
  /** Facts for an id no source describes, from the ladder and the route defaults. */
194
235
  inferredFacts(id, builtin) {
195
236
  const exact = builtin.get(id);
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Bounded JSON reads shared by the catalog and usage endpoints.
2
+ * Bounded body reads shared by the catalog, the usage endpoint, and the Go
3
+ * documentation fetch.
3
4
  *
4
- * Every outbound metadata read is capped before parsing: the metadata document
5
- * is third-party, and a truncated or oversized reply must surface as a named
5
+ * Every outbound metadata read is capped before parsing: the documents are
6
+ * third-party, and a truncated or oversized reply must surface as a named
6
7
  * failure rather than as memory growth.
7
8
  *
8
9
  * @module @dan-ai-studio/dshopencodego/catalog/json-response
@@ -17,3 +18,5 @@
17
18
  * or invalid JSON.
18
19
  */
19
20
  export declare function readBoundedJson(response: Response, url: string, maxBytes: number): Promise<unknown>;
21
+ /** Decode a body as text, aborting once it passes the cap. */
22
+ export declare function readBoundedText(response: Response, maxBytes: number): Promise<string>;
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Bounded JSON reads shared by the catalog and usage endpoints.
2
+ * Bounded body reads shared by the catalog, the usage endpoint, and the Go
3
+ * documentation fetch.
3
4
  *
4
- * Every outbound metadata read is capped before parsing: the metadata document
5
- * is third-party, and a truncated or oversized reply must surface as a named
5
+ * Every outbound metadata read is capped before parsing: the documents are
6
+ * third-party, and a truncated or oversized reply must surface as a named
6
7
  * failure rather than as memory growth.
7
8
  *
8
9
  * @module @dan-ai-studio/dshopencodego/catalog/json-response
@@ -39,8 +40,8 @@ export async function readBoundedJson(response, url, maxBytes) {
39
40
  throw new LlmError(`${url} did not answer with JSON`, 'DISCOVERY_FAILED', { cause: error });
40
41
  }
41
42
  }
42
- /** Decode a body, aborting once it passes the cap. */
43
- async function readBoundedText(response, maxBytes) {
43
+ /** Decode a body as text, aborting once it passes the cap. */
44
+ export async function readBoundedText(response, maxBytes) {
44
45
  if (response.body === null)
45
46
  return '';
46
47
  const reader = response.body.getReader();
@@ -24,8 +24,13 @@ import type { Api } from '@earendil-works/pi-ai';
24
24
  export type WireProtocol = 'anthropic-messages' | 'openai-completions' | 'openai-responses';
25
25
  /** Every protocol this plugin can drive, in catalog order. */
26
26
  export declare const WIRE_PROTOCOLS: readonly WireProtocol[];
27
- /** Which evidence level produced a protocol decision. */
28
- export type ProtocolSource = 'builtin' | 'online' | 'inferred' | 'override';
27
+ /**
28
+ * Which evidence level produced a protocol decision, most authoritative first:
29
+ * a configured `override`, the installed catalog (`builtin`), the provider's own
30
+ * endpoint table (`document`), models.dev's SDK hint (`online`), and last the
31
+ * family rule (`inferred`).
32
+ */
33
+ export type ProtocolSource = 'builtin' | 'document' | 'online' | 'inferred' | 'override';
29
34
  /** One model's protocol decision with the evidence that produced it. */
30
35
  export interface ProtocolDecision {
31
36
  readonly api: WireProtocol;
@@ -21,6 +21,11 @@ export interface CatalogReading {
21
21
  readonly error?: string;
22
22
  /** When the underlying snapshot was built. */
23
23
  readonly fetchedAtMs: number;
24
+ /**
25
+ * Where the per-model allowances came from: the provider's live document, or
26
+ * the frozen seed (whose transcription date the page shows alongside).
27
+ */
28
+ readonly quotaSource: 'document' | 'seed';
24
29
  /** Counts the settings page shows without walking the list again. */
25
30
  readonly counts: {
26
31
  readonly total: number;
@@ -20,9 +20,10 @@ import { goQuotaFor } from "../go-limits.js";
20
20
  * explain them instead of hiding them.
21
21
  */
22
22
  export function catalogReading(snapshot, visibility, listingFailure) {
23
+ const documentQuotas = snapshot.documentQuotas;
23
24
  const rows = [
24
25
  ...[...snapshot.facts.values()].map(fact => {
25
- const quota = goQuotaFor(fact.id);
26
+ const quota = documentQuotas === undefined ? goQuotaFor(fact.id) : documentQuotas.get(fact.id);
26
27
  const priced = fact.cost.input > 0 || fact.cost.output > 0;
27
28
  return {
28
29
  id: fact.id,
@@ -58,6 +59,7 @@ export function catalogReading(snapshot, visibility, listingFailure) {
58
59
  stale: !snapshot.live,
59
60
  ...!snapshot.live && listingFailure !== undefined ? { error: listingFailure } : {},
60
61
  fetchedAtMs: snapshot.fetchedAtMs,
62
+ quotaSource: documentQuotas === undefined ? 'seed' : 'document',
61
63
  counts: {
62
64
  total: models.length,
63
65
  enabled,
@@ -77,8 +77,13 @@ export interface OpencodeGoConfig {
77
77
  */
78
78
  retryPolicy?: RetryPolicyConfig;
79
79
  }
80
- /** Plain resolved values used by the adapter. */
81
- export declare const PlainConfig: z<OpencodeGoConfig>;
80
+ /**
81
+ * Plain resolved values used by the adapter.
82
+ *
83
+ * The input side is a partial on purpose: a profile writes only the fields it
84
+ * overrides, and the schema's own defaults fill the rest.
85
+ */
86
+ export declare const PlainConfig: z<Partial<OpencodeGoConfig>, OpencodeGoConfig>;
82
87
  /** 0.1.7's Loader retains these references when profile fields change. */
83
88
  export type LiveConfig = {
84
89
  [K in keyof OpencodeGoConfig]-?: {
@@ -44,7 +44,12 @@ const fields = {
44
44
  modelProtocols: z.dict(z.string()).default({}),
45
45
  retryPolicy: RetryPolicySchema,
46
46
  };
47
- /** Plain resolved values used by the adapter. */
47
+ /**
48
+ * Plain resolved values used by the adapter.
49
+ *
50
+ * The input side is a partial on purpose: a profile writes only the fields it
51
+ * overrides, and the schema's own defaults fill the rest.
52
+ */
48
53
  export const PlainConfig = z.object(fields);
49
54
  /** Runtime schema for {@link OpencodeGoConfig}; every field stays live. */
50
55
  export const Config = z.object(Object.fromEntries(Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])));
@@ -1,21 +1,33 @@
1
1
  /**
2
- * Go's published per-model quota.
2
+ * The frozen seed of Go's published per-model quota.
3
3
  *
4
- * No API exposes this — the gateway's `/v1/models` answers only ids, and
5
- * `/usage` answers three account-wide percentages — so the numbers are
6
- * transcribed from the provider's own documentation.
4
+ * No API exposes these numbers — the gateway's `/v1/models` answers ids and
5
+ * `/usage` answers account-wide percentages — so the live figures are parsed
6
+ * from the provider's own documentation by `catalog/go-doc.ts`. This table is
7
+ * the seed and the last resort behind that fetch:
8
+ *
9
+ * - it renders instantly at startup, before the first fetch resolves;
10
+ * - it keeps working when every source fails, including the day the document
11
+ * URL moves.
12
+ *
13
+ * It is deliberately **not** updated as models arrive: wherever it is shown its
14
+ * transcription date travels with it, so a stale number is never silent, and a
15
+ * model it does not list simply shows no allowance until a fetch succeeds.
7
16
  *
8
17
  * Source: https://opencode.ai/docs/zh-cn/go — the 「使用限制」 and
9
- * 「预估请求数」 tables. Transcribed 2026-09-25.
18
+ * 「预估请求数」 tables. Transcribed 2026-09-25, frozen 2026-09-26.
10
19
  *
11
20
  * Two caveats are part of the data's meaning, not noise:
12
21
  * - `monthlyUsd` is the hard monthly allowance; the request counts are the
13
22
  * provider's own estimate for a typical request mix, not a hard ceiling.
14
- * - DeepSeek V4.1 Flash carries a limited-time 4x allowance (until 2026-09-27)
15
- * already reflected here; the archived numbers follow the provider page.
23
+ * - DeepSeek V4.1 Flash carried a limited-time 4x allowance in this snapshot;
24
+ * the provider has since made the listed $60 permanent, which only the live
25
+ * document reports.
16
26
  *
17
27
  * @module @dan-ai-studio/dshopencodego/go-limits
18
28
  */
29
+ /** The date this seed was transcribed from the provider's page. */
30
+ export declare const SEED_TRANSCRIBED = "2026-09-25";
19
31
  /** One model's published Go allowance. */
20
32
  export interface GoQuota {
21
33
  /** Monthly usage allowance in USD, or `unlimited` where none is published. */
@@ -23,12 +35,12 @@ export interface GoQuota {
23
35
  /** Provider's estimated requests per month for a typical mix, when listed. */
24
36
  readonly monthlyRequests?: number | 'unlimited';
25
37
  }
26
- /** The transcribed table, keyed by model id. */
27
- export declare const GO_QUOTAS: Readonly<Record<string, GoQuota>>;
38
+ /** The frozen seed table, keyed by model id. Not a maintenance target. */
39
+ export declare const SEED_QUOTAS: Readonly<Record<string, GoQuota>>;
28
40
  /**
29
- * The published quota for one model id.
41
+ * The seed allowance for one model id.
30
42
  * @param id - the gateway's model id.
31
- * @returns the transcribed allowance, or undefined when the page lists none.
43
+ * @returns the frozen seed entry, or undefined when the page listed none.
32
44
  */
33
45
  export declare function goQuotaFor(id: string): GoQuota | undefined;
34
46
  /**
@@ -1,23 +1,35 @@
1
1
  /**
2
- * Go's published per-model quota.
2
+ * The frozen seed of Go's published per-model quota.
3
3
  *
4
- * No API exposes this — the gateway's `/v1/models` answers only ids, and
5
- * `/usage` answers three account-wide percentages — so the numbers are
6
- * transcribed from the provider's own documentation.
4
+ * No API exposes these numbers — the gateway's `/v1/models` answers ids and
5
+ * `/usage` answers account-wide percentages — so the live figures are parsed
6
+ * from the provider's own documentation by `catalog/go-doc.ts`. This table is
7
+ * the seed and the last resort behind that fetch:
8
+ *
9
+ * - it renders instantly at startup, before the first fetch resolves;
10
+ * - it keeps working when every source fails, including the day the document
11
+ * URL moves.
12
+ *
13
+ * It is deliberately **not** updated as models arrive: wherever it is shown its
14
+ * transcription date travels with it, so a stale number is never silent, and a
15
+ * model it does not list simply shows no allowance until a fetch succeeds.
7
16
  *
8
17
  * Source: https://opencode.ai/docs/zh-cn/go — the 「使用限制」 and
9
- * 「预估请求数」 tables. Transcribed 2026-09-25.
18
+ * 「预估请求数」 tables. Transcribed 2026-09-25, frozen 2026-09-26.
10
19
  *
11
20
  * Two caveats are part of the data's meaning, not noise:
12
21
  * - `monthlyUsd` is the hard monthly allowance; the request counts are the
13
22
  * provider's own estimate for a typical request mix, not a hard ceiling.
14
- * - DeepSeek V4.1 Flash carries a limited-time 4x allowance (until 2026-09-27)
15
- * already reflected here; the archived numbers follow the provider page.
23
+ * - DeepSeek V4.1 Flash carried a limited-time 4x allowance in this snapshot;
24
+ * the provider has since made the listed $60 permanent, which only the live
25
+ * document reports.
16
26
  *
17
27
  * @module @dan-ai-studio/dshopencodego/go-limits
18
28
  */
19
- /** The transcribed table, keyed by model id. */
20
- export const GO_QUOTAS = {
29
+ /** The date this seed was transcribed from the provider's page. */
30
+ export const SEED_TRANSCRIBED = '2026-09-25';
31
+ /** The frozen seed table, keyed by model id. Not a maintenance target. */
32
+ export const SEED_QUOTAS = {
21
33
  'glm-5.3-flash': { monthlyUsd: 60, monthlyRequests: 31_580 },
22
34
  'glm-5.3': { monthlyUsd: 15, monthlyRequests: 1_080 },
23
35
  'glm-5.2': { monthlyUsd: 60, monthlyRequests: 4_300 },
@@ -53,12 +65,12 @@ export const GO_QUOTAS = {
53
65
  'gpt-5.6-luna': { monthlyUsd: 15, monthlyRequests: 10_250 },
54
66
  };
55
67
  /**
56
- * The published quota for one model id.
68
+ * The seed allowance for one model id.
57
69
  * @param id - the gateway's model id.
58
- * @returns the transcribed allowance, or undefined when the page lists none.
70
+ * @returns the frozen seed entry, or undefined when the page listed none.
59
71
  */
60
72
  export function goQuotaFor(id) {
61
- return Object.hasOwn(GO_QUOTAS, id) ? GO_QUOTAS[id] : undefined;
73
+ return Object.hasOwn(SEED_QUOTAS, id) ? SEED_QUOTAS[id] : undefined;
62
74
  }
63
75
  /**
64
76
  * Rank a quota for "most usable first" ordering: unlimited beats any finite