diff --git a/pointer/config/models.yaml b/pointer/config/models.yaml index 5e87ea1..1dc4195 100644 --- a/pointer/config/models.yaml +++ b/pointer/config/models.yaml @@ -1,11 +1,14 @@ # OpenRouter model slugs (https://openrouter.ai/api/v1/models). `*` grants any # slug; a `:*` suffix grants every variant of one model, e.g. `:batch`. basic: + - local/lorem-ipsum - poolside/laguna-s-2.1:free advanced: + - local/lorem-ipsum - poolside/laguna-s-2.1:free - nvidia/nemotron-3.5-lightning:free premium: + - local/lorem-ipsum - poolside/laguna-s-2.1:free - nvidia/nemotron-3.5-lightning:free - google/gemma-4-31b-it:free diff --git a/pointer/lib/generate/index.ts b/pointer/lib/generate/index.ts index 27801f9..91825e8 100644 --- a/pointer/lib/generate/index.ts +++ b/pointer/lib/generate/index.ts @@ -1,6 +1,7 @@ import { streamText } from "ai"; import { chatModel } from "./openrouter"; +import { LOREM_MODEL, streamLoremIpsum } from "./lorem"; const MAX_PROMPT_CHARS = 8_000; const MAX_OUTPUT_TOKENS = 4_096; @@ -98,6 +99,10 @@ export function streamGeneration( input: GenerateInput, signal: AbortSignal, ): GenerationStream { + if (input.model === LOREM_MODEL) { + return streamLoremIpsum(input, signal); + } + let failure: unknown; const result = streamText({ diff --git a/pointer/lib/generate/lorem.ts b/pointer/lib/generate/lorem.ts new file mode 100644 index 0000000..1c80d6c --- /dev/null +++ b/pointer/lib/generate/lorem.ts @@ -0,0 +1,51 @@ +import type { GenerateInput, Generation, GenerationStream } from "./index"; + +// Must match the slug added to config/models.yaml. +export const LOREM_MODEL = "local/lorem-ipsum"; + +const LOREM_TEXT = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " + + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " + + "nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in " + + "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " + + "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + + "culpa qui officia deserunt mollit anim id est laborum."; + +// Simulates realistic streaming cadence without hitting the network. +const CHUNK_DELAY_MS = 40; + +// Same ratio as estimateTokens() in index.ts — kept local to avoid a circular import. +const estimate = (text: string) => Math.max(1, Math.ceil(text.length / 4)); + +/** + * Streams lorem ipsum word-by-word without calling any external API. + * Drop-in replacement for streamGeneration() when model === LOREM_MODEL. + */ +export function streamLoremIpsum( + input: GenerateInput, + signal: AbortSignal, +): GenerationStream { + const words = LOREM_TEXT.split(" "); + let streamed = ""; + + async function* read(): AsyncGenerator { + for (const word of words) { + if (signal.aborted) { break; } + const chunk = streamed.length === 0 ? word : ` ${word}`; + streamed += chunk; + yield chunk; + await new Promise((resolve) => setTimeout(resolve, CHUNK_DELAY_MS)); + } + } + + return { + deltas: read(), + streamedTokens: () => estimate(streamed), + settle: async (): Promise => ({ + output: streamed, + inputTokens: estimate(input.prompt), + outputTokens: estimate(streamed), + }), + }; +}