@coffer-org/server 2.3.1 → 2.3.2

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.
@@ -1,5 +1,19 @@
1
1
  export declare const EMBED_MODEL = "text-embedding-3-small";
2
2
  export declare const EMBED_DIM = 1024;
3
+ export type EmbeddingFailureReason = 'billing' | 'auth' | 'rate_limit' | 'server' | 'network' | 'unknown';
4
+ export declare class OpenAIEmbeddingError extends Error {
5
+ readonly status: number | null;
6
+ readonly code: string | null;
7
+ readonly reason: EmbeddingFailureReason;
8
+ constructor(args: {
9
+ status?: number | null;
10
+ code?: string | null;
11
+ detail?: string;
12
+ reason?: EmbeddingFailureReason;
13
+ cause?: unknown;
14
+ });
15
+ }
16
+ export declare function embeddingFailureMessage(error: unknown): string;
3
17
  export declare function embedBatch(texts: string[], apiKey: string, fetchImpl?: typeof fetch): Promise<{
4
18
  vectors: number[][];
5
19
  tokens: number;
@@ -1,19 +1,78 @@
1
1
  const OPENAI_URL = 'https://api.openai.com/v1/embeddings';
2
2
  export const EMBED_MODEL = 'text-embedding-3-small';
3
3
  export const EMBED_DIM = 1024;
4
+ export class OpenAIEmbeddingError extends Error {
5
+ status;
6
+ code;
7
+ reason;
8
+ constructor(args) {
9
+ const status = args.status ?? null;
10
+ const suffix = args.detail ? `: ${args.detail}` : '';
11
+ super(`openai embeddings${status == null ? '' : ` ${status}`}${suffix}`, { cause: args.cause });
12
+ this.name = 'OpenAIEmbeddingError';
13
+ this.status = status;
14
+ this.code = args.code ?? null;
15
+ this.reason = args.reason ?? classifyEmbeddingFailure(status, args.code, args.detail);
16
+ }
17
+ }
18
+ function parseErrorBody(body) {
19
+ try {
20
+ const value = JSON.parse(body);
21
+ const code = typeof value.error?.code === 'string' ? value.error.code : null;
22
+ const message = typeof value.error?.message === 'string' ? value.error.message : '';
23
+ if (code || message)
24
+ return { code, detail: message || code || 'request failed' };
25
+ }
26
+ catch {
27
+ }
28
+ return { code: null, detail: body.slice(0, 200) || 'request failed' };
29
+ }
30
+ function classifyEmbeddingFailure(status, code, detail) {
31
+ const marker = `${code ?? ''} ${detail ?? ''}`.toLowerCase();
32
+ if (status === 402 || /insufficient_quota|billing|payment_required|payment required|hard.?limit|quota exceeded/.test(marker))
33
+ return 'billing';
34
+ if (status === 401 || status === 403)
35
+ return 'auth';
36
+ if (status === 429 || /rate.?limit/.test(marker))
37
+ return 'rate_limit';
38
+ if (status != null && status >= 500)
39
+ return 'server';
40
+ return 'unknown';
41
+ }
42
+ export function embeddingFailureMessage(error) {
43
+ if (!(error instanceof OpenAIEmbeddingError))
44
+ return error instanceof Error ? error.message : String(error);
45
+ switch (error.reason) {
46
+ case 'billing':
47
+ return 'OpenAI billing or quota is unavailable. RAG indexing/search is paused; records are still saved and indexing will retry after billing is restored.';
48
+ case 'auth':
49
+ return 'The OpenAI API key was rejected. RAG indexing/search is unavailable; records are still saved.';
50
+ case 'rate_limit':
51
+ return 'OpenAI rate limit reached. RAG indexing/search will retry; records are still saved.';
52
+ default:
53
+ return `OpenAI embeddings are unavailable${error.status == null ? '' : ` (HTTP ${error.status})`}. RAG will retry; records are still saved.`;
54
+ }
55
+ }
4
56
  export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
5
57
  if (texts.length === 0)
6
58
  return { vectors: [], tokens: 0 };
7
59
  if (!apiKey)
8
60
  throw new Error('embeddings: missing API key (OPENAI_API_KEY)');
9
- const res = await fetchImpl(OPENAI_URL, {
10
- method: 'POST',
11
- headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
12
- body: JSON.stringify({ model: EMBED_MODEL, input: texts, dimensions: EMBED_DIM }),
13
- });
61
+ let res;
62
+ try {
63
+ res = await fetchImpl(OPENAI_URL, {
64
+ method: 'POST',
65
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
66
+ body: JSON.stringify({ model: EMBED_MODEL, input: texts, dimensions: EMBED_DIM }),
67
+ });
68
+ }
69
+ catch (cause) {
70
+ throw new OpenAIEmbeddingError({ reason: 'network', detail: 'network request failed', cause });
71
+ }
14
72
  if (!res.ok) {
15
73
  const body = await res.text().catch(() => '');
16
- throw new Error(`openai embeddings ${res.status}: ${body.slice(0, 200)}`);
74
+ const parsed = parseErrorBody(body);
75
+ throw new OpenAIEmbeddingError({ status: res.status, code: parsed.code, detail: parsed.detail });
17
76
  }
18
77
  const json = (await res.json());
19
78
  const vectors = [...json.data].sort((a, b) => a.index - b.index).map((d) => d.embedding);
package/dist/mcp-tools.js CHANGED
@@ -11,7 +11,7 @@ import { describeCondition } from '@coffer-org/sdk/condition';
11
11
  import { getEm } from "./db.js";
12
12
  import { getPluginSettings } from "./plugin-runtime.js";
13
13
  import { searchEmbeddings } from "./embeddings.js";
14
- import { embedOne } from "./embed-openai.js";
14
+ import { embedOne, embeddingFailureMessage } from "./embed-openai.js";
15
15
  import { getLogger } from "./log.js";
16
16
  import { writePluginSettings, listSettings } from "./settings-write.js";
17
17
  import { ValidationError, NotFoundError } from "./mutate.js";
@@ -143,7 +143,7 @@ export async function collectMcpTools(opts = {}) {
143
143
  return { content: [{ type: 'text', text: formatHits(hits) }] };
144
144
  }
145
145
  catch (e) {
146
- return fail(`Error: ${e.message}`);
146
+ return fail(`RAG unavailable: ${embeddingFailureMessage(e)} Use the regular coffer tools (list_records/get_record) instead.`);
147
147
  }
148
148
  },
149
149
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/server",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"