import type { PackageStorage } from "kody:runtime"; import type { ContentLocale, IdempotencyKey, NormalizedRequest, Story, StoryPhase, } from "./domain"; import { storyLibraryUrl } from "./domain"; const TOKEN_PLACEHOLDER = "{{secret:teddyPinguinToken}}"; const DEFAULT_API_BASE_URL = "https://www.teddy-pinguin.com"; // Teddy mints run ids with crypto.randomUUID(), which never returns the nil UUID. const IDENTITY_PROBE_RUN_ID = "00000000-0000-0000-0000-000000000000"; const RUN_STATUSES = [ "queued", "running", "cancelling", "accepted", "rejected", "cancelled", "failed", ] as const; export type RunStatus = (typeof RUN_STATUSES)[number]; const STORY_PHASES = [ "planning", "writing", "reviewing", "continuity_memory", "finalizing", ] as const; export type RunState = { runId: string; status: RunStatus; phase: StoryPhase | null; storyHref: string | null; error: { code: string; phase: StoryPhase | "preparing" | null; retryable: boolean } | null; }; export type ApiFailure = | { kind: "unauthenticated" } | { kind: "not_found"; code: string } | { kind: "quota_exhausted"; code: string } | { kind: "rate_limited"; retryAfterSeconds: number | null } | { kind: "rejected_request"; code: string; field: string | null } | { kind: "transient"; retryAfterSeconds: number | null } | { kind: "unexpected"; code: string }; export type StartRunResult = | { ok: true; run: RunState } | { ok: false; failure: ApiFailure }; export type ReadRunResult = { ok: true; run: RunState } | { ok: false; failure: ApiFailure }; export type LoadStoryResult = { ok: true; story: Story } | { ok: false; failure: ApiFailure }; export type CreateSeriesResult = | { ok: true; seriesId: string } | { ok: false; failure: ApiFailure }; export type TeddyClient = { readonly apiBaseUrl: string; startRun(request: NormalizedRequest, key: IdempotencyKey): Promise; createSeries(input: { contentLocale: ContentLocale; continueFromStoryId?: string; }): Promise; readRun(runId: string): Promise; loadStory(storyHref: string, fallbackLocale: ContentLocale): Promise; probeIdentity(): Promise<"authenticated" | "unauthenticated" | "unreachable">; }; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isRunStatus(value: unknown): value is RunStatus { return typeof value === "string" && (RUN_STATUSES as readonly string[]).includes(value); } function isStoryPhase(value: unknown): value is StoryPhase { return typeof value === "string" && (STORY_PHASES as readonly string[]).includes(value); } function parseErrorPhase(value: unknown): StoryPhase | "preparing" | null { if (value === "preparing") { return "preparing"; } if (isStoryPhase(value)) { return value; } return null; } function parseProblem(payload: unknown): { code: string; field: string | null } | null { if (!isRecord(payload) || typeof payload.code !== "string" || !payload.code) { return null; } const errors = payload.errors; const first = Array.isArray(errors) && isRecord(errors[0]) ? errors[0] : null; const field = first && typeof first.field === "string" ? first.field : null; return { code: payload.code, field }; } function parseRetryAfter(header: string | null): number | null { if (!header) { return null; } const seconds = Number.parseInt(header, 10); if (!Number.isFinite(seconds) || seconds < 0) { return null; } return seconds; } async function readJson(response: Response): Promise { try { return await response.json(); } catch { return undefined; } } function authHeaders(): HeadersInit { return { Authorization: `Bearer ${TOKEN_PLACEHOLDER}` }; } function resolveApiUrl(apiBaseUrl: string, href: string): URL | null { try { const url = new URL(href, apiBaseUrl); if (url.origin !== new URL(apiBaseUrl).origin) { return null; } return url; } catch { return null; } } async function classify(response: Response): Promise { const problem = parseProblem(await readJson(response)); const code = problem?.code; const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after")); if (response.status === 401) { return { kind: "unauthenticated" }; } if (response.status === 403 && code === "STORY_LIMIT_REACHED") { return { kind: "quota_exhausted", code }; } if (response.status === 404) { return { kind: "not_found", code: code ?? "NOT_FOUND" }; } if (response.status === 429) { return { kind: "rate_limited", retryAfterSeconds }; } if (response.status === 400 || response.status === 409) { return { kind: "rejected_request", code: code ?? `HTTP_${response.status}`, field: problem?.field ?? null, }; } if (response.status === 408 || response.status === 425 || response.status >= 500) { return { kind: "transient", retryAfterSeconds }; } return { kind: "unexpected", code: code ?? `HTTP_${response.status}` }; } function classifyThrow(): ApiFailure { return { kind: "transient", retryAfterSeconds: null }; } function parseRunSnapshot(payload: unknown): RunState | null { if (!isRecord(payload) || !isRecord(payload.run)) { return null; } const run = payload.run; if (typeof run.id !== "string" || !run.id || !isRunStatus(run.status)) { return null; } const result = isRecord(run.result) ? run.result : null; const href = result && typeof result.href === "string" && result.href ? result.href : null; const errorRecord = isRecord(run.error) ? run.error : null; const error = errorRecord && typeof errorRecord.code === "string" && errorRecord.code ? { code: errorRecord.code, phase: parseErrorPhase(errorRecord.phase), retryable: errorRecord.retryable === true, } : null; return { runId: run.id, status: run.status, phase: isStoryPhase(run.phase) ? run.phase : null, storyHref: href, error, }; } function parseStory( payload: unknown, apiBaseUrl: string, fallbackLocale: ContentLocale, ): Story | null { if (!isRecord(payload) || !isRecord(payload.story)) { return null; } const story = payload.story; if ( typeof story.id !== "string" || !story.id || typeof story.title !== "string" || typeof story.summary !== "string" || typeof story.content !== "string" ) { return null; } const contentLocale: ContentLocale = story.contentLocale === "en" || story.contentLocale === "de" ? story.contentLocale : fallbackLocale; const output = isRecord(story.output) ? story.output : null; const estimated = output && typeof output.estimatedReadSeconds === "number" && Number.isFinite(output.estimatedReadSeconds) ? output.estimatedReadSeconds : null; const target = output && typeof output.targetReadMinutes === "number" && Number.isFinite(output.targetReadMinutes) ? output.targetReadMinutes : null; const readMinutes = estimated !== null ? Math.max(1, Math.round(estimated / 60)) : target; const seriesRecord = isRecord(story.series) ? story.series : null; const series = seriesRecord && typeof seriesRecord.id === "string" && seriesRecord.id && typeof seriesRecord.position === "number" && Number.isInteger(seriesRecord.position) ? { id: seriesRecord.id, position: seriesRecord.position } : null; return { id: story.id, title: story.title, summary: story.summary, prose: story.content, contentLocale, readMinutes, libraryUrl: storyLibraryUrl(apiBaseUrl, story.id, contentLocale), series, }; } function httpsOrigin(value: string): string | null { try { const url = new URL(value); if (url.protocol !== "https:") { return null; } return url.origin; } catch { return null; } } async function readApiBaseUrl(storage: PackageStorage): Promise { const override = await storage?.get("apiBaseUrl"); if (typeof override === "string") { const origin = httpsOrigin(override.trim()); if (origin) { return origin; } } return DEFAULT_API_BASE_URL; } async function startRun( apiBaseUrl: string, request: NormalizedRequest, key: IdempotencyKey, ): Promise { let response: Response; try { response = await fetch(`${apiBaseUrl}/api/v1/story-generation-runs`, { method: "POST", headers: { ...authHeaders(), "Content-Type": "application/json", "Idempotency-Key": key, }, body: JSON.stringify({ storyIdea: request.storyIdea, contentLocale: request.contentLocale, ...(request.seriesId ? { seriesId: request.seriesId } : {}), output: { format: "prose", targetReadMinutes: request.targetReadMinutes, }, }), }); } catch { return { ok: false, failure: classifyThrow() }; } if (!response.ok) { return { ok: false, failure: await classify(response) }; } if (response.status !== 202) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } const run = parseRunSnapshot(await readJson(response)); if (!run) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } return { ok: true, run }; } async function createSeries( apiBaseUrl: string, input: { contentLocale: ContentLocale; continueFromStoryId?: string }, ): Promise { let response: Response; try { response = await fetch(`${apiBaseUrl}/api/v1/story-series`, { method: "POST", headers: { ...authHeaders(), "Content-Type": "application/json", }, body: JSON.stringify({ contentLocale: input.contentLocale, ...(input.continueFromStoryId ? { continueFromStoryId: input.continueFromStoryId } : {}), }), }); } catch { return { ok: false, failure: classifyThrow() }; } if (!response.ok) { return { ok: false, failure: await classify(response) }; } if (response.status !== 201) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } const payload = await readJson(response); const series = isRecord(payload) && isRecord(payload.series) ? payload.series : null; if (!series || typeof series.id !== "string" || !series.id) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } return { ok: true, seriesId: series.id }; } async function readRun(apiBaseUrl: string, runId: string): Promise { let response: Response; try { response = await fetch( `${apiBaseUrl}/api/v1/story-generation-runs/${encodeURIComponent(runId)}`, { headers: authHeaders() }, ); } catch { return { ok: false, failure: classifyThrow() }; } if (!response.ok) { return { ok: false, failure: await classify(response) }; } if (response.status !== 200) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } const run = parseRunSnapshot(await readJson(response)); if (!run) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } return { ok: true, run }; } async function loadStory( apiBaseUrl: string, storyHref: string, fallbackLocale: ContentLocale, ): Promise { const url = resolveApiUrl(apiBaseUrl, storyHref); if (!url) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } let response: Response; try { response = await fetch(url, { headers: authHeaders() }); } catch { return { ok: false, failure: classifyThrow() }; } if (!response.ok) { return { ok: false, failure: await classify(response) }; } if (response.status !== 200) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } const story = parseStory(await readJson(response), apiBaseUrl, fallbackLocale); if (!story) { return { ok: false, failure: { kind: "unexpected", code: "INVALID_TEDDY_RESPONSE" } }; } return { ok: true, story }; } async function probeIdentity( apiBaseUrl: string, ): Promise<"authenticated" | "unauthenticated" | "unreachable"> { let response: Response; try { response = await fetch( `${apiBaseUrl}/api/v1/story-generation-runs/${IDENTITY_PROBE_RUN_ID}`, { headers: authHeaders() }, ); } catch { return "unreachable"; } if (response.status === 401) { return "unauthenticated"; } if (response.status === 404) { const problem = parseProblem(await readJson(response)); if (problem?.code === "STORY_RUN_NOT_FOUND") { return "authenticated"; } return "unreachable"; } return "unreachable"; } export async function openTeddyClient(storage: PackageStorage): Promise { const apiBaseUrl = await readApiBaseUrl(storage); return { apiBaseUrl, startRun: (request, key) => startRun(apiBaseUrl, request, key), createSeries: (input) => createSeries(apiBaseUrl, input), readRun: (runId) => readRun(apiBaseUrl, runId), loadStory: (storyHref, fallbackLocale) => loadStory(apiBaseUrl, storyHref, fallbackLocale), probeIdentity: () => probeIdentity(apiBaseUrl), }; }