@joystyk/content 0.0.0-stage → 0.2.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +113 -2
  5. package/dist/banks.d.ts +14 -0
  6. package/dist/banks.js +55 -0
  7. package/dist/banks.js.map +1 -0
  8. package/dist/extract-json.d.ts +12 -0
  9. package/dist/extract-json.js +59 -0
  10. package/dist/extract-json.js.map +1 -0
  11. package/dist/filter.d.ts +56 -0
  12. package/dist/filter.js +127 -0
  13. package/dist/filter.js.map +1 -0
  14. package/dist/games.d.ts +9 -0
  15. package/dist/games.js +40 -0
  16. package/dist/games.js.map +1 -0
  17. package/dist/hash.d.ts +4 -0
  18. package/dist/hash.js +17 -0
  19. package/dist/hash.js.map +1 -0
  20. package/dist/index.d.ts +17 -0
  21. package/dist/index.js +16 -0
  22. package/dist/log.d.ts +26 -0
  23. package/dist/log.js +58 -0
  24. package/dist/log.js.map +1 -0
  25. package/dist/model.d.ts +77 -0
  26. package/dist/model.js +268 -0
  27. package/dist/model.js.map +1 -0
  28. package/dist/packs.d.ts +144 -0
  29. package/dist/packs.js +466 -0
  30. package/dist/packs.js.map +1 -0
  31. package/dist/pipeline.d.ts +252 -0
  32. package/dist/pipeline.js +558 -0
  33. package/dist/pipeline.js.map +1 -0
  34. package/dist/publish.d.ts +21 -0
  35. package/dist/publish.js +56 -0
  36. package/dist/publish.js.map +1 -0
  37. package/dist/ranked.d.ts +12 -0
  38. package/dist/ranked.js +40 -0
  39. package/dist/ranked.js.map +1 -0
  40. package/dist/screen.d.ts +25 -0
  41. package/dist/screen.js +46 -0
  42. package/dist/screen.js.map +1 -0
  43. package/dist/store.d.ts +136 -0
  44. package/dist/store.js +300 -0
  45. package/dist/store.js.map +1 -0
  46. package/dist/text.d.ts +31 -0
  47. package/dist/text.js +111 -0
  48. package/dist/text.js.map +1 -0
  49. package/dist/vocabulary.d.ts +16 -0
  50. package/dist/vocabulary.js +44 -0
  51. package/dist/vocabulary.js.map +1 -0
  52. package/lists/SCOWL-COPYRIGHT +252 -0
  53. package/lists/blocklist.txt +214 -0
  54. package/lists/common-extra.txt +13 -0
  55. package/lists/scowl-common.txt +70763 -0
  56. package/lists/scowl-ranked.txt +72076 -0
  57. package/lists/scowl-vocabulary.txt +55636 -0
  58. package/lists/sensitive.txt +280 -0
  59. package/package.json +53 -3
package/dist/hash.js ADDED
@@ -0,0 +1,17 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalJson } from "@joystyk/daily";
3
+ //#region src/hash.ts
4
+ var contentHash = (config) => createHash("sha256").update(canonicalJson(config), "utf8").digest("hex").slice(0, 12);
5
+ /** A stable unsigned 32-bit number from a string (FNV-1a), for deterministic picks such as the bank's. */
6
+ var stableIndex = (key, modulo) => {
7
+ let hash = 2166136261;
8
+ for (const byte of new TextEncoder().encode(key)) {
9
+ hash ^= byte;
10
+ hash = Math.imul(hash, 16777619) >>> 0;
11
+ }
12
+ return modulo <= 0 ? 0 : hash % modulo;
13
+ };
14
+ //#endregion
15
+ export { contentHash, stableIndex };
16
+
17
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","names":[],"sources":["../src/hash.ts"],"sourcesContent":["// The content hash, defined once with the daily package: the first 12 hex digits of SHA-256\n// over `canonicalJson(config)` from @joystyk/daily (content-pipeline.md, \"Puzzle IDs and\n// variants\"). The browser client checks the same digest with Web Crypto before caching a\n// file, so both sides must use the daily package's canonical JSON; hash.test.ts proves they agree.\nimport { createHash } from 'node:crypto';\nimport type { Json } from '@joystyk/contracts';\nimport { canonicalJson } from '@joystyk/daily';\n\nexport const contentHash = (config: Json): string =>\n createHash('sha256').update(canonicalJson(config), 'utf8').digest('hex').slice(0, 12);\n\n/** A stable unsigned 32-bit number from a string (FNV-1a), for deterministic picks such as the bank's. */\nexport const stableIndex = (key: string, modulo: number): number => {\n let hash = 0x811c9dc5;\n for (const byte of new TextEncoder().encode(key)) {\n hash ^= byte;\n hash = Math.imul(hash, 0x01000193) >>> 0;\n }\n return modulo <= 0 ? 0 : hash % modulo;\n};\n"],"mappings":";;;AAQA,IAAa,eAAe,WAC1B,WAAW,QAAQ,CAAC,CAAC,OAAO,cAAc,MAAM,GAAG,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;;AAGtF,IAAa,eAAe,KAAa,WAA2B;CAClE,IAAI,OAAO;CACX,KAAK,MAAM,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,GAAG,GAAG;EAChD,QAAQ;EACR,OAAO,KAAK,KAAK,MAAM,QAAU,MAAM;CACzC;CACA,OAAO,UAAU,IAAI,IAAI,OAAO;AAClC"}
@@ -0,0 +1,17 @@
1
+ export { createLogger, LOG_LEVELS, redact, silentLogger, type Fields, type Logger, type LogLevel } from './log.js';
2
+ export { extractJson, stripReasoning } from './extract-json.js';
3
+ export { chatCompletionsUrl, createModelClient, ENDPOINT_ERRORS, responseSchema, type CallInfo, type CompleteRequest, type Completion, type CompletionFailure, type Message, type ModelClient, type ModelDeps, type ModelError, type ModelErrorKind, type ModelSettings, type Usage, } from './model.js';
4
+ export { fold, formsOf, leaksLetters, mentions, sameText, stemOf, usesForm, wordsOf } from './text.js';
5
+ export { isPlainCommonWord, scowlVocabulary, vocabularyOf, type Vocabulary } from './vocabulary.js';
6
+ export { parseRanked, rankedWords, scoreOfSize, type RankedWords } from './ranked.js';
7
+ export { createWordFilter, defaultFilter, filterForms, gridLines, type FieldText, type FilterHit, type FilterList, type FilterReport, type WordFilter, } from './filter.js';
8
+ export { modelScreen, SCREEN_PROMPT_VERSION, SCREEN_SYSTEM, screenVerdict, type ScreenVerdict } from './screen.js';
9
+ export { contentHash, stableIndex } from './hash.js';
10
+ export { createPublisher, puzzleFileBody, type PublishedFile, type Publisher } from './publish.js';
11
+ export { openStore, WITHDRAW_REASONS, type ContentStore, type ItemPatch, type ItemRow, type ItemSource, type ItemStatus, type RunRow, type WithdrawReason, } from './store.js';
12
+ export { createPackPublisher, PACK_SCHEMA_VERSION, packFileBody, packItemId, packSlugOf, rebuildPackIndex, runPackPipeline, withdrawPack, type PackAction, type PackFile, type PackGenerateContext, type PackGenerator, type PackIndex, type PackIndexEntry, type PackPipelineOptions, type PackPublisher, type PackRunReport, type PackSlot, type PackSlotReport, } from './packs.js';
13
+ export { rebuildIndex, runPipeline, withdrawItem, type AttemptOutcome, type AttemptReport, type CallReport, type DailyGenerator, type GenerateContext, type GenerateFailure, type PipelineOptions, type PublishReport, type RunReport, type ScheduleContext, type Scheduled, type SlotAction, type SlotReport, } from './pipeline.js';
14
+ export { CONTENT_GAMES, dailyGenerators, type DailyContentGame } from './games.js';
15
+ export { dailyBankEntries, withDailyBank, withPackBank, type BankEntry } from './banks.js';
16
+ export type { BankChange } from './store.js';
17
+ export { checkDailyConfig, recentContentKeys } from './pipeline.js';
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import { dailyBankEntries, withDailyBank, withPackBank } from "./banks.js";
2
+ import { extractJson, stripReasoning } from "./extract-json.js";
3
+ import { fold, formsOf, leaksLetters, mentions, sameText, stemOf, usesForm, wordsOf } from "./text.js";
4
+ import { createWordFilter, defaultFilter, filterForms, gridLines } from "./filter.js";
5
+ import { CONTENT_GAMES, dailyGenerators } from "./games.js";
6
+ import { contentHash, stableIndex } from "./hash.js";
7
+ import { LOG_LEVELS, createLogger, redact, silentLogger } from "./log.js";
8
+ import { ENDPOINT_ERRORS, chatCompletionsUrl, createModelClient, responseSchema } from "./model.js";
9
+ import { parseRanked, rankedWords, scoreOfSize } from "./ranked.js";
10
+ import { isPlainCommonWord, scowlVocabulary, vocabularyOf } from "./vocabulary.js";
11
+ import { SCREEN_PROMPT_VERSION, SCREEN_SYSTEM, modelScreen, screenVerdict } from "./screen.js";
12
+ import { createPublisher, puzzleFileBody } from "./publish.js";
13
+ import { WITHDRAW_REASONS, openStore } from "./store.js";
14
+ import { checkDailyConfig, rebuildIndex, recentContentKeys, runPipeline, withdrawItem } from "./pipeline.js";
15
+ import { PACK_SCHEMA_VERSION, createPackPublisher, packFileBody, packItemId, packSlugOf, rebuildPackIndex, runPackPipeline, withdrawPack } from "./packs.js";
16
+ export { CONTENT_GAMES, ENDPOINT_ERRORS, LOG_LEVELS, PACK_SCHEMA_VERSION, SCREEN_PROMPT_VERSION, SCREEN_SYSTEM, WITHDRAW_REASONS, chatCompletionsUrl, checkDailyConfig, contentHash, createLogger, createModelClient, createPackPublisher, createPublisher, createWordFilter, dailyBankEntries, dailyGenerators, defaultFilter, extractJson, filterForms, fold, formsOf, gridLines, isPlainCommonWord, leaksLetters, mentions, modelScreen, openStore, packFileBody, packItemId, packSlugOf, parseRanked, puzzleFileBody, rankedWords, rebuildIndex, rebuildPackIndex, recentContentKeys, redact, responseSchema, runPackPipeline, runPipeline, sameText, scoreOfSize, scowlVocabulary, screenVerdict, silentLogger, stableIndex, stemOf, stripReasoning, usesForm, vocabularyOf, withDailyBank, withPackBank, withdrawItem, withdrawPack, wordsOf };
package/dist/log.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export declare const LOG_LEVELS: readonly LogLevel[];
3
+ export type Fields = Readonly<Record<string, unknown>>;
4
+ export interface Logger {
5
+ readonly debug: (msg: string, fields?: Fields) => void;
6
+ readonly info: (msg: string, fields?: Fields) => void;
7
+ readonly warn: (msg: string, fields?: Fields) => void;
8
+ readonly error: (msg: string, fields?: Fields) => void;
9
+ }
10
+ export interface LoggerOptions {
11
+ readonly level?: LogLevel;
12
+ /** Receives each line without its newline. Default: stdout. */
13
+ readonly write?: (line: string) => void;
14
+ readonly now?: () => Date;
15
+ /**
16
+ * Strings that must never reach a log line: the model key, the endpoint's host. Each is
17
+ * replaced with "[redacted]". Blank and very short strings are ignored (they would mangle
18
+ * ordinary text).
19
+ */
20
+ readonly secrets?: readonly string[];
21
+ }
22
+ /** Replaces every occurrence of each secret (and its JSON-escaped form) in `text`. */
23
+ export declare const redact: (text: string, secrets: readonly string[]) => string;
24
+ export declare function createLogger(options?: LoggerOptions): Logger;
25
+ /** A logger that drops everything, for tests. */
26
+ export declare const silentLogger: Logger;
package/dist/log.js ADDED
@@ -0,0 +1,58 @@
1
+ //#region src/log.ts
2
+ var LOG_LEVELS = [
3
+ "debug",
4
+ "info",
5
+ "warn",
6
+ "error"
7
+ ];
8
+ var RANK = {
9
+ debug: 10,
10
+ info: 20,
11
+ warn: 30,
12
+ error: 40
13
+ };
14
+ /** Errors don't survive JSON.stringify; turn them into plain fields. */
15
+ var plain = (value) => value instanceof Error ? {
16
+ name: value.name,
17
+ message: value.message,
18
+ stack: value.stack
19
+ } : value;
20
+ /** Replaces every occurrence of each secret (and its JSON-escaped form) in `text`. */
21
+ var redact = (text, secrets) => {
22
+ let out = text;
23
+ for (const secret of secrets) {
24
+ if (secret.length < 6) continue;
25
+ out = out.replaceAll(secret, "[redacted]");
26
+ const escaped = JSON.stringify(secret).slice(1, -1);
27
+ if (escaped !== secret) out = out.replaceAll(escaped, "[redacted]");
28
+ }
29
+ return out;
30
+ };
31
+ function createLogger(options = {}) {
32
+ const min = RANK[options.level ?? "info"];
33
+ const write = options.write ?? ((line) => process.stdout.write(`${line}\n`));
34
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
35
+ const secrets = (options.secrets ?? []).filter((secret) => secret.trim() !== "");
36
+ const log = (level) => (msg, fields = {}) => {
37
+ if (RANK[level] < min) return;
38
+ const entry = {
39
+ time: now().toISOString(),
40
+ level,
41
+ msg
42
+ };
43
+ for (const [key, value] of Object.entries(fields)) entry[key] = plain(value);
44
+ write(redact(JSON.stringify(entry), secrets));
45
+ };
46
+ return {
47
+ debug: log("debug"),
48
+ info: log("info"),
49
+ warn: log("warn"),
50
+ error: log("error")
51
+ };
52
+ }
53
+ /** A logger that drops everything, for tests. */
54
+ var silentLogger = createLogger({ write: () => void 0 });
55
+ //#endregion
56
+ export { LOG_LEVELS, createLogger, redact, silentLogger };
57
+
58
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","names":[],"sources":["../src/log.ts"],"sourcesContent":["// Structured logs: one JSON object per line on stdout, which Docker's json-file driver\n// collects (hosting-and-scaling.md, \"Operations\"). The content service handles a secret (the\n// model key) and a private endpoint, so every line passes through a redactor that replaces\n// any configured secret with \"[redacted]\" before it is written, whatever field it hides in.\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\nexport const LOG_LEVELS: readonly LogLevel[] = ['debug', 'info', 'warn', 'error'];\n\nexport type Fields = Readonly<Record<string, unknown>>;\n\nexport interface Logger {\n readonly debug: (msg: string, fields?: Fields) => void;\n readonly info: (msg: string, fields?: Fields) => void;\n readonly warn: (msg: string, fields?: Fields) => void;\n readonly error: (msg: string, fields?: Fields) => void;\n}\n\nconst RANK: Readonly<Record<LogLevel, number>> = { debug: 10, info: 20, warn: 30, error: 40 };\n\nexport interface LoggerOptions {\n readonly level?: LogLevel;\n /** Receives each line without its newline. Default: stdout. */\n readonly write?: (line: string) => void;\n readonly now?: () => Date;\n /**\n * Strings that must never reach a log line: the model key, the endpoint's host. Each is\n * replaced with \"[redacted]\". Blank and very short strings are ignored (they would mangle\n * ordinary text).\n */\n readonly secrets?: readonly string[];\n}\n\n/** Errors don't survive JSON.stringify; turn them into plain fields. */\nconst plain = (value: unknown): unknown =>\n value instanceof Error ? { name: value.name, message: value.message, stack: value.stack } : value;\n\n/** Replaces every occurrence of each secret (and its JSON-escaped form) in `text`. */\nexport const redact = (text: string, secrets: readonly string[]): string => {\n let out = text;\n for (const secret of secrets) {\n if (secret.length < 6) continue;\n out = out.replaceAll(secret, '[redacted]');\n const escaped = JSON.stringify(secret).slice(1, -1);\n if (escaped !== secret) out = out.replaceAll(escaped, '[redacted]');\n }\n return out;\n};\n\nexport function createLogger(options: LoggerOptions = {}): Logger {\n const min = RANK[options.level ?? 'info'];\n const write = options.write ?? ((line: string) => process.stdout.write(`${line}\\n`));\n const now = options.now ?? (() => new Date());\n const secrets = (options.secrets ?? []).filter((secret) => secret.trim() !== '');\n const log =\n (level: LogLevel) =>\n (msg: string, fields: Fields = {}) => {\n if (RANK[level] < min) return;\n const entry: Record<string, unknown> = { time: now().toISOString(), level, msg };\n for (const [key, value] of Object.entries(fields)) entry[key] = plain(value);\n write(redact(JSON.stringify(entry), secrets));\n };\n return { debug: log('debug'), info: log('info'), warn: log('warn'), error: log('error') };\n}\n\n/** A logger that drops everything, for tests. */\nexport const silentLogger: Logger = createLogger({ write: () => undefined });\n"],"mappings":";AAMA,IAAa,aAAkC;CAAC;CAAS;CAAQ;CAAQ;AAAO;AAWhF,IAAM,OAA2C;CAAE,OAAO;CAAI,MAAM;CAAI,MAAM;CAAI,OAAO;AAAG;;AAgB5F,IAAM,SAAS,UACb,iBAAiB,QAAQ;CAAE,MAAM,MAAM;CAAM,SAAS,MAAM;CAAS,OAAO,MAAM;AAAM,IAAI;;AAG9F,IAAa,UAAU,MAAc,YAAuC;CAC1E,IAAI,MAAM;CACV,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,OAAO,SAAS,GAAG;EACvB,MAAM,IAAI,WAAW,QAAQ,YAAY;EACzC,MAAM,UAAU,KAAK,UAAU,MAAM,CAAC,CAAC,MAAM,GAAG,EAAE;EAClD,IAAI,YAAY,QAAQ,MAAM,IAAI,WAAW,SAAS,YAAY;CACpE;CACA,OAAO;AACT;AAEA,SAAgB,aAAa,UAAyB,CAAC,GAAW;CAChE,MAAM,MAAM,KAAK,QAAQ,SAAS;CAClC,MAAM,QAAQ,QAAQ,WAAW,SAAiB,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG;CAClF,MAAM,MAAM,QAAQ,8BAAc,IAAI,KAAK;CAC3C,MAAM,WAAW,QAAQ,WAAW,CAAC,EAAA,CAAG,QAAQ,WAAW,OAAO,KAAK,MAAM,EAAE;CAC/E,MAAM,OACH,WACA,KAAa,SAAiB,CAAC,MAAM;EACpC,IAAI,KAAK,SAAS,KAAK;EACvB,MAAM,QAAiC;GAAE,MAAM,IAAI,CAAC,CAAC,YAAY;GAAG;GAAO;EAAI;EAC/E,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG,MAAM,OAAO,MAAM,KAAK;EAC3E,MAAM,OAAO,KAAK,UAAU,KAAK,GAAG,OAAO,CAAC;CAC9C;CACF,OAAO;EAAE,OAAO,IAAI,OAAO;EAAG,MAAM,IAAI,MAAM;EAAG,MAAM,IAAI,MAAM;EAAG,OAAO,IAAI,OAAO;CAAE;AAC1F;;AAGA,IAAa,eAAuB,aAAa,EAAE,aAAa,KAAA,EAAU,CAAC"}
@@ -0,0 +1,77 @@
1
+ import { type Result } from '@joystyk/contracts';
2
+ import * as z from 'zod';
3
+ export interface ModelSettings {
4
+ /** The endpoint's base URL, with or without `/v1`: `<base>/chat/completions` is called. */
5
+ readonly baseUrl: string;
6
+ readonly key: string;
7
+ readonly model: string;
8
+ /** Per HTTP request. Default 120 s. */
9
+ readonly timeoutMs?: number;
10
+ /** `max_tokens` for every request; room for a reasoning model to think. Default 4096. */
11
+ readonly maxTokens?: number;
12
+ /** Default 0.7: varied wording across days; the validators keep it honest. */
13
+ readonly temperature?: number;
14
+ /** Extra tries after a transport failure. Default 2. */
15
+ readonly transportRetries?: number;
16
+ /** First backoff; doubles each time. Default 1000 ms. */
17
+ readonly backoffMs?: number;
18
+ }
19
+ export interface ModelDeps {
20
+ readonly fetch?: typeof fetch;
21
+ readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
22
+ readonly now?: () => number;
23
+ }
24
+ export type ModelErrorKind = 'http' | 'timeout' | 'network' | 'aborted' | 'empty' | 'truncated' | 'parse' | 'schema';
25
+ /** Endpoint problems, as opposed to a reply that was merely wrong. They feed the circuit breaker. */
26
+ export declare const ENDPOINT_ERRORS: ReadonlySet<ModelErrorKind>;
27
+ export interface Usage {
28
+ readonly promptTokens: number | null;
29
+ readonly completionTokens: number | null;
30
+ readonly totalTokens: number | null;
31
+ }
32
+ /** What one `complete` call cost, for logs and the item's provenance. */
33
+ export interface CallInfo {
34
+ readonly name: string;
35
+ readonly latencyMs: number;
36
+ readonly usage: Usage | null;
37
+ /** HTTP requests made, including transport retries. */
38
+ readonly requests: number;
39
+ readonly mode: 'json_schema' | 'prompt';
40
+ readonly finishReason: string | null;
41
+ }
42
+ export interface ModelError {
43
+ readonly kind: ModelErrorKind;
44
+ /** Safe to log: no key, no address, no reply text. */
45
+ readonly message: string;
46
+ readonly status?: number;
47
+ readonly issues?: readonly string[];
48
+ }
49
+ export type Completion<T> = {
50
+ readonly value: T;
51
+ readonly call: CallInfo;
52
+ };
53
+ export type CompletionFailure = ModelError & {
54
+ readonly call: CallInfo;
55
+ };
56
+ export interface Message {
57
+ readonly role: 'system' | 'user' | 'assistant';
58
+ readonly content: string;
59
+ }
60
+ export interface CompleteRequest<T> {
61
+ /** Names the schema in the request, e.g. "word_entry". */
62
+ readonly name: string;
63
+ readonly schema: z.ZodType<T>;
64
+ readonly messages: readonly Message[];
65
+ readonly temperature?: number;
66
+ readonly signal?: AbortSignal;
67
+ }
68
+ export interface ModelClient {
69
+ /** The model name sent with every request, for provenance. */
70
+ readonly model: string;
71
+ complete<T>(request: CompleteRequest<T>): Promise<Result<Completion<T>, CompletionFailure>>;
72
+ }
73
+ /** `<base>/chat/completions` for a base URL given with or without `/v1` (or the full path). */
74
+ export declare const chatCompletionsUrl: (baseUrl: string) => string;
75
+ /** The JSON Schema sent as the response format: draft 7, output types, no `$schema` key. */
76
+ export declare const responseSchema: (schema: z.ZodType) => Record<string, unknown>;
77
+ export declare function createModelClient(settings: ModelSettings, deps?: ModelDeps): ModelClient;
package/dist/model.js ADDED
@@ -0,0 +1,268 @@
1
+ import { extractJson } from "./extract-json.js";
2
+ import { err, ok } from "@joystyk/contracts";
3
+ import * as z from "zod";
4
+ //#region src/model.ts
5
+ /** Endpoint problems, as opposed to a reply that was merely wrong. They feed the circuit breaker. */
6
+ var ENDPOINT_ERRORS = /* @__PURE__ */ new Set([
7
+ "http",
8
+ "timeout",
9
+ "network"
10
+ ]);
11
+ /** `<base>/chat/completions` for a base URL given with or without `/v1` (or the full path). */
12
+ var chatCompletionsUrl = (baseUrl) => {
13
+ const base = baseUrl.trim().replace(/\/+$/, "");
14
+ if (/\/chat\/completions$/.test(base)) return base;
15
+ if (/\/v\d+$/.test(base)) return `${base}/chat/completions`;
16
+ return `${base}/v1/chat/completions`;
17
+ };
18
+ /** The JSON Schema sent as the response format: draft 7, output types, no `$schema` key. */
19
+ var responseSchema = (schema) => {
20
+ const { $schema: _ignored, ...rest } = z.toJSONSchema(schema, {
21
+ target: "draft-7",
22
+ io: "output",
23
+ unrepresentable: "any"
24
+ });
25
+ return rest;
26
+ };
27
+ var describeIssues = (issues) => issues.map((issue) => {
28
+ const path = (issue.path ?? []).map((segment) => typeof segment === "object" && segment !== null && "key" in segment ? String(segment.key) : String(segment)).join(".");
29
+ return path === "" ? issue.message : `${path}: ${issue.message}`;
30
+ });
31
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([
32
+ 408,
33
+ 409,
34
+ 425,
35
+ 429,
36
+ 500,
37
+ 502,
38
+ 503,
39
+ 504
40
+ ]);
41
+ var defaultSleep = (ms, signal) => new Promise((resolve) => {
42
+ if (signal?.aborted === true) return resolve();
43
+ const timer = setTimeout(done, ms);
44
+ function done() {
45
+ clearTimeout(timer);
46
+ signal?.removeEventListener("abort", done);
47
+ resolve();
48
+ }
49
+ signal?.addEventListener("abort", done, { once: true });
50
+ });
51
+ var numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
52
+ var readUsage = (value) => {
53
+ if (typeof value !== "object" || value === null) return null;
54
+ const usage = value;
55
+ return {
56
+ promptTokens: numberOrNull(usage["prompt_tokens"]),
57
+ completionTokens: numberOrNull(usage["completion_tokens"]),
58
+ totalTokens: numberOrNull(usage["total_tokens"])
59
+ };
60
+ };
61
+ var schemaInstruction = (schema) => `Reply with one JSON object only, no prose and no code fence. It must match this JSON Schema:\n${JSON.stringify(responseSchema(schema))}`;
62
+ function createModelClient(settings, deps = {}) {
63
+ const url = chatCompletionsUrl(settings.baseUrl);
64
+ const doFetch = deps.fetch ?? globalThis.fetch;
65
+ const sleep = deps.sleep ?? defaultSleep;
66
+ const now = deps.now ?? Date.now;
67
+ const timeoutMs = settings.timeoutMs ?? 12e4;
68
+ const maxTokens = settings.maxTokens ?? 4096;
69
+ const retries = settings.transportRetries ?? 2;
70
+ const backoffMs = settings.backoffMs ?? 1e3;
71
+ let mode = "json_schema";
72
+ const post = async (body, signal) => {
73
+ const timeout = AbortSignal.timeout(timeoutMs);
74
+ const combined = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
75
+ let response;
76
+ try {
77
+ response = await doFetch(url, {
78
+ method: "POST",
79
+ headers: {
80
+ authorization: `Bearer ${settings.key}`,
81
+ "content-type": "application/json",
82
+ accept: "application/json"
83
+ },
84
+ body: JSON.stringify(body),
85
+ signal: combined
86
+ });
87
+ } catch {
88
+ if (signal?.aborted === true) return {
89
+ kind: "error",
90
+ error: {
91
+ kind: "aborted",
92
+ message: "The request was cancelled."
93
+ },
94
+ retryable: false,
95
+ retryAfterMs: null,
96
+ formatRefused: false
97
+ };
98
+ if (timeout.aborted) return {
99
+ kind: "error",
100
+ error: {
101
+ kind: "timeout",
102
+ message: `No reply within ${String(timeoutMs)} ms.`
103
+ },
104
+ retryable: true,
105
+ retryAfterMs: null,
106
+ formatRefused: false
107
+ };
108
+ return {
109
+ kind: "error",
110
+ error: {
111
+ kind: "network",
112
+ message: "The endpoint could not be reached."
113
+ },
114
+ retryable: true,
115
+ retryAfterMs: null,
116
+ formatRefused: false
117
+ };
118
+ }
119
+ let text;
120
+ try {
121
+ text = await response.text();
122
+ } catch {
123
+ const kind = signal?.aborted === true ? "aborted" : timeout.aborted ? "timeout" : "network";
124
+ return {
125
+ kind: "error",
126
+ error: {
127
+ kind,
128
+ message: "The reply was cut off."
129
+ },
130
+ retryable: kind !== "aborted",
131
+ retryAfterMs: null,
132
+ formatRefused: false
133
+ };
134
+ }
135
+ if (!response.ok) {
136
+ const retryAfter = Number(response.headers.get("retry-after"));
137
+ return {
138
+ kind: "error",
139
+ error: {
140
+ kind: "http",
141
+ status: response.status,
142
+ message: `The endpoint answered HTTP ${String(response.status)} (${String(text.length)} bytes).`
143
+ },
144
+ retryable: RETRYABLE_STATUS.has(response.status),
145
+ retryAfterMs: Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 30) * 1e3 : null,
146
+ formatRefused: (response.status === 400 || response.status === 422) && /response_format|json_schema|schema/i.test(text)
147
+ };
148
+ }
149
+ try {
150
+ return {
151
+ kind: "reply",
152
+ body: JSON.parse(text)
153
+ };
154
+ } catch {
155
+ return {
156
+ kind: "error",
157
+ error: {
158
+ kind: "http",
159
+ status: response.status,
160
+ message: `The endpoint's reply was not JSON (${String(text.length)} bytes).`
161
+ },
162
+ retryable: true,
163
+ retryAfterMs: null,
164
+ formatRefused: false
165
+ };
166
+ }
167
+ };
168
+ return {
169
+ model: settings.model,
170
+ complete: async (request) => {
171
+ const started = now();
172
+ let requests = 0;
173
+ let usage = null;
174
+ let finishReason = null;
175
+ const info = () => ({
176
+ name: request.name,
177
+ latencyMs: now() - started,
178
+ usage,
179
+ requests,
180
+ mode,
181
+ finishReason
182
+ });
183
+ const fail = (error) => err({
184
+ ...error,
185
+ call: info()
186
+ });
187
+ const bodyFor = () => {
188
+ const messages = mode === "json_schema" ? request.messages : [
189
+ ...request.messages.slice(0, -1),
190
+ {
191
+ role: "system",
192
+ content: schemaInstruction(request.schema)
193
+ },
194
+ ...request.messages.slice(-1)
195
+ ];
196
+ return {
197
+ model: settings.model,
198
+ messages,
199
+ temperature: request.temperature ?? settings.temperature ?? .7,
200
+ max_tokens: maxTokens,
201
+ ...mode === "json_schema" ? { response_format: {
202
+ type: "json_schema",
203
+ json_schema: {
204
+ name: request.name,
205
+ strict: true,
206
+ schema: responseSchema(request.schema)
207
+ }
208
+ } } : {}
209
+ };
210
+ };
211
+ let reply;
212
+ for (let attempt = 0;; attempt++) {
213
+ requests++;
214
+ const result = await post(bodyFor(), request.signal);
215
+ if (result.kind === "reply") {
216
+ reply = result.body;
217
+ break;
218
+ }
219
+ if (result.formatRefused && mode === "json_schema") {
220
+ mode = "prompt";
221
+ attempt--;
222
+ continue;
223
+ }
224
+ if (!result.retryable || attempt >= retries) return fail(result.error);
225
+ await sleep(result.retryAfterMs ?? backoffMs * 2 ** attempt, request.signal);
226
+ if (request.signal?.aborted === true) return fail({
227
+ kind: "aborted",
228
+ message: "The request was cancelled."
229
+ });
230
+ }
231
+ const body = typeof reply === "object" && reply !== null ? reply : {};
232
+ usage = readUsage(body["usage"]);
233
+ const choice = Array.isArray(body["choices"]) ? body["choices"][0] : void 0;
234
+ finishReason = typeof choice?.["finish_reason"] === "string" ? choice["finish_reason"] : null;
235
+ const message = choice?.["message"] ?? {};
236
+ const content = typeof message["content"] === "string" ? message["content"] : "";
237
+ if (content.trim() === "") return fail(finishReason === "length" ? {
238
+ kind: "truncated",
239
+ message: "The reply ran out of tokens before any answer."
240
+ } : {
241
+ kind: "empty",
242
+ message: "The reply had no content."
243
+ });
244
+ const found = extractJson(content);
245
+ if (!found.ok) return fail(finishReason === "length" ? {
246
+ kind: "truncated",
247
+ message: "The reply ran out of tokens before its JSON was complete."
248
+ } : {
249
+ kind: "parse",
250
+ message: `The reply held no JSON object (${String(content.length)} characters).`
251
+ });
252
+ const decoded = await request.schema["~standard"].validate(found.value);
253
+ if (decoded.issues !== void 0) return fail({
254
+ kind: "schema",
255
+ message: "The reply did not match the schema.",
256
+ issues: describeIssues(decoded.issues)
257
+ });
258
+ return ok({
259
+ value: decoded.value,
260
+ call: info()
261
+ });
262
+ }
263
+ };
264
+ }
265
+ //#endregion
266
+ export { ENDPOINT_ERRORS, chatCompletionsUrl, createModelClient, responseSchema };
267
+
268
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","names":[],"sources":["../src/model.ts"],"sourcesContent":["// The model client (D-45): an OpenAI-compatible chat completions endpoint, called from the\n// server with plain `fetch` and a Bearer key, as the owner's other sites do. One small module, no SDK.\n//\n// Each `complete` call:\n// 1. converts the item's Zod schema to JSON Schema (`z.toJSONSchema`) and sends it as the\n// `json_schema` response format, so the model is constrained to the right shape;\n// 2. retries transport failures (timeouts, network errors, 408, 429 and 5xx) with\n// exponential backoff, honoring Retry-After;\n// 3. finds the JSON in the reply (reasoning blocks, prose and code fences are tolerated) and\n// decodes it through the same schema with the Standard Schema interface. Endpoints honor\n// only part of JSON Schema, so this decode is the real check.\n// If the endpoint refuses `response_format` (HTTP 400 or 422), the client falls back to\n// describing the schema in the prompt for the rest of its life.\n//\n// Errors never carry the key, the endpoint's address or a raw reply; a response body is\n// reduced to its length and status.\nimport { err, ok, type Result } from '@joystyk/contracts';\nimport * as z from 'zod';\nimport { extractJson } from './extract-json.ts';\n\nexport interface ModelSettings {\n /** The endpoint's base URL, with or without `/v1`: `<base>/chat/completions` is called. */\n readonly baseUrl: string;\n readonly key: string;\n readonly model: string;\n /** Per HTTP request. Default 120 s. */\n readonly timeoutMs?: number;\n /** `max_tokens` for every request; room for a reasoning model to think. Default 4096. */\n readonly maxTokens?: number;\n /** Default 0.7: varied wording across days; the validators keep it honest. */\n readonly temperature?: number;\n /** Extra tries after a transport failure. Default 2. */\n readonly transportRetries?: number;\n /** First backoff; doubles each time. Default 1000 ms. */\n readonly backoffMs?: number;\n}\n\nexport interface ModelDeps {\n readonly fetch?: typeof fetch;\n readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;\n readonly now?: () => number;\n}\n\nexport type ModelErrorKind =\n | 'http' // a non-retryable status, or a retryable one after every retry\n | 'timeout'\n | 'network'\n | 'aborted' // the caller's signal (shutdown)\n | 'empty' // no content in the reply\n | 'truncated' // finish_reason \"length\": max_tokens ran out\n | 'parse' // no JSON object in the reply\n | 'schema'; // JSON that the schema refuses\n\n/** Endpoint problems, as opposed to a reply that was merely wrong. They feed the circuit breaker. */\nexport const ENDPOINT_ERRORS: ReadonlySet<ModelErrorKind> = new Set(['http', 'timeout', 'network']);\n\nexport interface Usage {\n readonly promptTokens: number | null;\n readonly completionTokens: number | null;\n readonly totalTokens: number | null;\n}\n\n/** What one `complete` call cost, for logs and the item's provenance. */\nexport interface CallInfo {\n readonly name: string;\n readonly latencyMs: number;\n readonly usage: Usage | null;\n /** HTTP requests made, including transport retries. */\n readonly requests: number;\n readonly mode: 'json_schema' | 'prompt';\n readonly finishReason: string | null;\n}\n\nexport interface ModelError {\n readonly kind: ModelErrorKind;\n /** Safe to log: no key, no address, no reply text. */\n readonly message: string;\n readonly status?: number;\n readonly issues?: readonly string[];\n}\n\nexport type Completion<T> = { readonly value: T; readonly call: CallInfo };\nexport type CompletionFailure = ModelError & { readonly call: CallInfo };\n\nexport interface Message {\n readonly role: 'system' | 'user' | 'assistant';\n readonly content: string;\n}\n\nexport interface CompleteRequest<T> {\n /** Names the schema in the request, e.g. \"word_entry\". */\n readonly name: string;\n readonly schema: z.ZodType<T>;\n readonly messages: readonly Message[];\n readonly temperature?: number;\n readonly signal?: AbortSignal;\n}\n\nexport interface ModelClient {\n /** The model name sent with every request, for provenance. */\n readonly model: string;\n complete<T>(request: CompleteRequest<T>): Promise<Result<Completion<T>, CompletionFailure>>;\n}\n\n/** `<base>/chat/completions` for a base URL given with or without `/v1` (or the full path). */\nexport const chatCompletionsUrl = (baseUrl: string): string => {\n const base = baseUrl.trim().replace(/\\/+$/, '');\n if (/\\/chat\\/completions$/.test(base)) return base;\n if (/\\/v\\d+$/.test(base)) return `${base}/chat/completions`;\n return `${base}/v1/chat/completions`;\n};\n\n/** The JSON Schema sent as the response format: draft 7, output types, no `$schema` key. */\nexport const responseSchema = (schema: z.ZodType): Record<string, unknown> => {\n const { $schema: _ignored, ...rest } = z.toJSONSchema(schema, {\n target: 'draft-7',\n io: 'output',\n unrepresentable: 'any',\n }) as Record<string, unknown>;\n return rest;\n};\n\nconst describeIssues = (issues: readonly { message: string; path?: readonly unknown[] | undefined }[]): string[] =>\n issues.map((issue) => {\n const path = (issue.path ?? [])\n .map((segment) =>\n typeof segment === 'object' && segment !== null && 'key' in segment ? String(segment.key) : String(segment),\n )\n .join('.');\n return path === '' ? issue.message : `${path}: ${issue.message}`;\n });\n\nconst RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);\n\nconst defaultSleep = (ms: number, signal?: AbortSignal): Promise<void> =>\n new Promise((resolve) => {\n if (signal?.aborted === true) return resolve();\n const timer = setTimeout(done, ms);\n function done() {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n signal?.addEventListener('abort', done, { once: true });\n });\n\nconst numberOrNull = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null;\n\nconst readUsage = (value: unknown): Usage | null => {\n if (typeof value !== 'object' || value === null) return null;\n const usage = value as Record<string, unknown>;\n return {\n promptTokens: numberOrNull(usage['prompt_tokens']),\n completionTokens: numberOrNull(usage['completion_tokens']),\n totalTokens: numberOrNull(usage['total_tokens']),\n };\n};\n\nconst schemaInstruction = (schema: z.ZodType): string =>\n `Reply with one JSON object only, no prose and no code fence. It must match this JSON Schema:\\n${JSON.stringify(responseSchema(schema))}`;\n\nexport function createModelClient(settings: ModelSettings, deps: ModelDeps = {}): ModelClient {\n const url = chatCompletionsUrl(settings.baseUrl);\n const doFetch = deps.fetch ?? globalThis.fetch;\n const sleep = deps.sleep ?? defaultSleep;\n const now = deps.now ?? Date.now;\n const timeoutMs = settings.timeoutMs ?? 120_000;\n const maxTokens = settings.maxTokens ?? 4096;\n const retries = settings.transportRetries ?? 2;\n const backoffMs = settings.backoffMs ?? 1000;\n let mode: 'json_schema' | 'prompt' = 'json_schema';\n\n type Attempt =\n | { readonly kind: 'reply'; readonly body: unknown }\n | {\n readonly kind: 'error';\n readonly error: ModelError;\n readonly retryable: boolean;\n readonly retryAfterMs: number | null;\n readonly formatRefused: boolean;\n };\n\n const post = async (body: unknown, signal: AbortSignal | undefined): Promise<Attempt> => {\n const timeout = AbortSignal.timeout(timeoutMs);\n const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);\n let response: Response;\n try {\n response = await doFetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${settings.key}`,\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n body: JSON.stringify(body),\n signal: combined,\n });\n } catch {\n if (signal?.aborted === true)\n return {\n kind: 'error',\n error: { kind: 'aborted', message: 'The request was cancelled.' },\n retryable: false,\n retryAfterMs: null,\n formatRefused: false,\n };\n if (timeout.aborted)\n return {\n kind: 'error',\n error: { kind: 'timeout', message: `No reply within ${String(timeoutMs)} ms.` },\n retryable: true,\n retryAfterMs: null,\n formatRefused: false,\n };\n return {\n kind: 'error',\n error: { kind: 'network', message: 'The endpoint could not be reached.' },\n retryable: true,\n retryAfterMs: null,\n formatRefused: false,\n };\n }\n let text: string;\n try {\n text = await response.text();\n } catch {\n const kind = signal?.aborted === true ? 'aborted' : timeout.aborted ? 'timeout' : 'network';\n return {\n kind: 'error',\n error: { kind, message: 'The reply was cut off.' },\n retryable: kind !== 'aborted',\n retryAfterMs: null,\n formatRefused: false,\n };\n }\n if (!response.ok) {\n const retryAfter = Number(response.headers.get('retry-after'));\n return {\n kind: 'error',\n error: {\n kind: 'http',\n status: response.status,\n message: `The endpoint answered HTTP ${String(response.status)} (${String(text.length)} bytes).`,\n },\n retryable: RETRYABLE_STATUS.has(response.status),\n retryAfterMs: Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 30) * 1000 : null,\n formatRefused:\n (response.status === 400 || response.status === 422) && /response_format|json_schema|schema/i.test(text),\n };\n }\n try {\n return { kind: 'reply', body: JSON.parse(text) as unknown };\n } catch {\n return {\n kind: 'error',\n error: {\n kind: 'http',\n status: response.status,\n message: `The endpoint's reply was not JSON (${String(text.length)} bytes).`,\n },\n retryable: true,\n retryAfterMs: null,\n formatRefused: false,\n };\n }\n };\n\n return {\n model: settings.model,\n complete: async <T>(request: CompleteRequest<T>): Promise<Result<Completion<T>, CompletionFailure>> => {\n const started = now();\n let requests = 0;\n let usage: Usage | null = null;\n let finishReason: string | null = null;\n const info = (): CallInfo => ({\n name: request.name,\n latencyMs: now() - started,\n usage,\n requests,\n mode,\n finishReason,\n });\n const fail = (error: ModelError): Result<never, CompletionFailure> => err({ ...error, call: info() });\n\n const bodyFor = () => {\n const messages =\n mode === 'json_schema'\n ? request.messages\n : [\n ...request.messages.slice(0, -1),\n { role: 'system', content: schemaInstruction(request.schema) },\n ...request.messages.slice(-1),\n ];\n return {\n model: settings.model,\n messages,\n temperature: request.temperature ?? settings.temperature ?? 0.7,\n max_tokens: maxTokens,\n ...(mode === 'json_schema'\n ? {\n response_format: {\n type: 'json_schema',\n json_schema: { name: request.name, strict: true, schema: responseSchema(request.schema) },\n },\n }\n : {}),\n };\n };\n\n let reply: unknown;\n for (let attempt = 0; ; attempt++) {\n requests++;\n const result = await post(bodyFor(), request.signal);\n if (result.kind === 'reply') {\n reply = result.body;\n break;\n }\n if (result.formatRefused && mode === 'json_schema') {\n mode = 'prompt';\n attempt--; // switching formats isn't a retry\n continue;\n }\n if (!result.retryable || attempt >= retries) return fail(result.error);\n await sleep(result.retryAfterMs ?? backoffMs * 2 ** attempt, request.signal);\n if (request.signal?.aborted === true) return fail({ kind: 'aborted', message: 'The request was cancelled.' });\n }\n\n const body = (typeof reply === 'object' && reply !== null ? reply : {}) as Record<string, unknown>;\n usage = readUsage(body['usage']);\n const choice = Array.isArray(body['choices'])\n ? (body['choices'][0] as Record<string, unknown> | undefined)\n : undefined;\n finishReason = typeof choice?.['finish_reason'] === 'string' ? choice['finish_reason'] : null;\n const message = (choice?.['message'] ?? {}) as Record<string, unknown>;\n const content = typeof message['content'] === 'string' ? message['content'] : '';\n if (content.trim() === '') {\n return fail(\n finishReason === 'length'\n ? { kind: 'truncated', message: 'The reply ran out of tokens before any answer.' }\n : { kind: 'empty', message: 'The reply had no content.' },\n );\n }\n const found = extractJson(content);\n if (!found.ok) {\n return fail(\n finishReason === 'length'\n ? { kind: 'truncated', message: 'The reply ran out of tokens before its JSON was complete.' }\n : { kind: 'parse', message: `The reply held no JSON object (${String(content.length)} characters).` },\n );\n }\n const decoded = await request.schema['~standard'].validate(found.value);\n if (decoded.issues !== undefined) {\n return fail({\n kind: 'schema',\n message: 'The reply did not match the schema.',\n issues: describeIssues(decoded.issues),\n });\n }\n return ok({ value: decoded.value, call: info() });\n },\n };\n}\n"],"mappings":"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"}