@kenkaiiii/gg-ai 5.76.0 → 5.78.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/README.md CHANGED
@@ -98,6 +98,137 @@ The model lists are the ones GG Coder ships in its registry (`@kenkaiiii/gg-core
98
98
  | `compaction` | `boolean` | Server-side compaction (Anthropic only) |
99
99
  | `clearToolUses` | `boolean` | Server-side clearing of old tool results (Anthropic only) |
100
100
  | `fetch` | `typeof fetch` | Custom fetch, e.g. for React Native |
101
+ | `providerOptions` | `Record<string, unknown>` | Provider-specific body fields; see below |
102
+
103
+ ### Provider-specific body fields
104
+
105
+ `providerOptions` is merged into the JSON body of `stream()`, `embed()` and `rerank()` requests. It only adds fields gg-ai hasn't set. Core fields (`model`, `messages`, `input`, `query`, `documents`, `stream`, `tools`, `system`, `contents`, `requests`, `dimensions`, `encoding_format`, `top_n`) can't be set this way, even when gg-ai left them out. The main use is OpenRouter's [provider routing](https://openrouter.ai/docs/features/provider-routing):
106
+
107
+ ```ts
108
+ import { stream, type OpenRouterProviderPreferences } from "@kenkaiiii/gg-ai";
109
+
110
+ const result = stream({
111
+ provider: "openrouter",
112
+ model: "anthropic/claude-sonnet-4.5",
113
+ messages,
114
+ apiKey,
115
+ providerOptions: {
116
+ provider: { zdr: true, data_collection: "deny" } satisfies OpenRouterProviderPreferences,
117
+ },
118
+ });
119
+ ```
120
+
121
+ `@kenkaiiii/gg-agent` passes `providerOptions` and `fetch` through to every `stream()` call it makes.
122
+
123
+ ---
124
+
125
+ ## Embeddings and reranking
126
+
127
+ `embed()` and `rerank()` use the same provider registry as `stream()`.
128
+
129
+ ```ts
130
+ import { embed, rerank } from "@kenkaiiii/gg-ai";
131
+
132
+ const { embeddings, usage } = await embed({
133
+ provider: "openrouter",
134
+ model: "google/gemini-embedding-2",
135
+ input: chunks, // any length; split into batches for you
136
+ dimensions: 1536,
137
+ normalize: true,
138
+ apiKey,
139
+ providerOptions: { provider: { zdr: true, data_collection: "deny", allow_fallbacks: true } },
140
+ });
141
+ // embeddings[i] is the vector for chunks[i]
142
+
143
+ const { results } = await rerank({
144
+ provider: "openrouter",
145
+ model: "cohere/rerank-v3.5",
146
+ query: "capital of France",
147
+ documents,
148
+ topN: 5,
149
+ apiKey,
150
+ });
151
+ // results: [{ index, score }], best first; documents[results[0].index] is the top hit
152
+ ```
153
+
154
+ **What `embed()` guarantees**
155
+ - Vectors come back in input order, even when the provider returns them out of order (gg-ai sorts by `index`).
156
+ - Long inputs are split at the provider's per-request limit (table below; override with `batchSize`). Batches run one at a time, in order.
157
+ - The response is checked: exactly one vector per input, all numbers, all the same length, and `dimensions` long when you asked for that. If any check fails it throws `ProviderError`. Nothing is padded or truncated.
158
+ - `normalize: true` L2-normalizes every vector. It's off by default. OpenAI embeddings come back unit-length already, and so does Gemini at full 3072 dimensions. `gemini-embedding-2` also normalizes shortened vectors, but `gemini-embedding-001` doesn't: at reduced `dimensions` its vectors are not unit-length, so pass `normalize: true`. Through OpenRouter or a local server, it depends on the upstream model. If you compare with cosine similarity or a dot product and aren't sure, normalize.
159
+ - `inputType: "query" | "document"` maps to the provider's native task parameter: Gemini `taskType` (`RETRIEVAL_QUERY` / `RETRIEVAL_DOCUMENT`) and OpenRouter `input_type` (`search_query` / `search_document`). It's ignored by OpenAI and local servers, which have no such parameter. It's also ignored for `gemini-embedding-2`, where Google wants the task written into the text instead (`task: search result | query: …`).
160
+ - An empty `input` returns `{ embeddings: [] }` without making a request.
161
+
162
+ **What `rerank()` guarantees**: results are sorted by score, highest first, and each `index` points into your `documents`. At most `topN` results come back, and every index is checked. An empty `documents` returns `{ results: [] }` without making a request.
163
+
164
+ ### Provider support
165
+
166
+ | Provider | `embed` | Max inputs per request | `inputType` | `rerank` |
167
+ |---|---|---|---|---|
168
+ | `openai` | ✓ `/v1/embeddings` | 2048 | ignored | — |
169
+ | `openrouter` | ✓ `/api/v1/embeddings` | 64 (gg-ai default; OpenRouter documents no limit) | `input_type` | ✓ `/api/v1/rerank` |
170
+ | `gemini` | ✓ native `batchEmbedContents` | 100 | `taskType` | — |
171
+ | `local` | ✓ `{baseUrl}/embeddings` (Ollama, LM Studio, llama.cpp, vLLM) | 64 (gg-ai default) | ignored | ✓ `{baseUrl}/rerank` (llama.cpp `--rerank`, vLLM; Ollama and LM Studio have none and return 404) |
172
+ | `palsu` | ✓ deterministic fake | all | ignored | ✓ deterministic fake |
173
+ | `anthropic`, `xiaomi`, `glm`, `moonshot`, `minimax`, `deepseek`, `sakana`, `xai`, `huggingface` | — | | | — |
174
+
175
+ Calling `embed()` or `rerank()` on a provider without that capability throws `GGAIError` with `source: "capability"`. Check first with `providerRegistry.supports(name, "embed")`.
176
+
177
+ Notes:
178
+ - **Gemini** embeddings use the Gemini API with an AI Studio key, sent as `x-goog-api-key`. The Code Assist sign-in that `stream()` uses for Gemini chat has no embeddings endpoint.
179
+ - **OpenAI** embeddings need a platform API key. ChatGPT-subscription (Codex) tokens can't call `/embeddings`.
180
+ - Anthropic has no embeddings API (it points to Voyage AI). The other providers in the last row don't document an embeddings or rerank endpoint on the API gg-ai talks to, so none is registered for them.
181
+ - Document parsing and OCR are out of scope for now. That would be a separate API (file in, pages out), and providers disagree on it much more than on embeddings.
182
+
183
+ ### Errors, retries and cancellation
184
+
185
+ Failures use the same error model as `stream()`:
186
+
187
+ | Failure | Error |
188
+ |---|---|
189
+ | HTTP 401 / 403 | `ProviderError`, `source: "auth"` |
190
+ | HTTP 402, or billing text such as `insufficient_quota` | `ProviderError` with "usage limit reached" (`isUsageLimitError()` is true) |
191
+ | HTTP 429 | `ProviderError`, `statusCode: 429`, `resetsAt` from `Retry-After` when sent |
192
+ | Other HTTP errors | `ProviderError` with `statusCode` and `requestId` |
193
+ | Connection failure | `GGAIError`, `source: "network"` |
194
+ | `signal` aborted | `AbortError` (`DOMException`); remaining batches are skipped |
195
+
196
+ Provider error bodies are trimmed to their message, then redacted and truncated, so API keys never end up in an error message.
197
+
198
+ gg-ai doesn't retry. `stream()` doesn't either: retry policy lives in the caller, which for chat is `@kenkaiiii/gg-agent`. Retry 429s and 5xx with backoff if you need to. Never retry auth errors or anything where `isUsageLimitError(err)` is true.
199
+
200
+ ### Custom providers
201
+
202
+ A provider registered at runtime can offer embeddings and reranking too. `stream` is still required, and the other two are optional. gg-ai splits input into batches of `embedBatchSize`, validates and orders the results, normalizes, and sorts. Your functions only handle one request.
203
+
204
+ ```ts
205
+ import { providerRegistry, embed, type Provider, type ProviderEntry } from "@kenkaiiii/gg-ai";
206
+
207
+ providerRegistry.register("my-search", {
208
+ stream: () => { throw new Error("chat not supported"); },
209
+ embedBatchSize: 128,
210
+ embed: async ({ model, input, dimensions, apiKey, signal, fetch = globalThis.fetch }) => {
211
+ const res = await fetch("https://example.com/embed", {
212
+ method: "POST",
213
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
214
+ body: JSON.stringify({ model, texts: input, dimensions }),
215
+ signal,
216
+ });
217
+ const json = await res.json();
218
+ return { embeddings: json.vectors, usage: { inputTokens: json.tokens } };
219
+ },
220
+ rerank: async ({ query, documents }) => ({
221
+ results: documents.map((doc, index) => ({ index, score: doc.includes(query) ? 1 : 0 })),
222
+ }),
223
+ } satisfies ProviderEntry);
224
+
225
+ // `Provider` lists the built-ins, so custom names need a cast (same as for stream()).
226
+ await embed({ provider: "my-search" as Provider, model: "v1", input: ["hello"] });
227
+ ```
228
+
229
+ ### Testing without a network
230
+
231
+ `registerPalsuProvider()` gives `palsu` deterministic `embed` and `rerank` fakes. Embeddings are hashed bag-of-words unit vectors (64 dimensions by default, or `dimensions`), so texts that share words score closer together. Rerank scores are the fraction of query words found in each document. `embedDimensions` and `embedBatchSize` in the config change the defaults, and `state.embedCallCount` / `state.rerankCallCount` count requests.
101
232
 
102
233
  ---
103
234