@molecule/api-ai-deepseek 1.0.14 → 1.1.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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @module
10
10
  */
11
- import type { AIProvider, ChatEvent, ChatParams } from '@molecule/api-ai';
11
+ import type { AIProvider, ChatEvent, ChatParams, ImageInputSize, ImageInputTokenEstimate } from '@molecule/api-ai';
12
12
  import './secrets.js';
13
13
  import type { DeepseekConfig } from './types.js';
14
14
  /** How long a stream may wait for its first content event. */
@@ -38,6 +38,26 @@ declare class DeepseekAIProviderImpl implements AIProvider {
38
38
  * @yields {ChatEvent} Chat events including text chunks, tool use requests, done signals, and errors.
39
39
  */
40
40
  chat(params: ChatParams): AsyncIterable<ChatEvent>;
41
+ /**
42
+ * DeepSeek's own documented price for one image, in input tokens.
43
+ *
44
+ * Priced only when BOTH conditions hold: the request is going to DeepSeek's
45
+ * own documented endpoint ({@link isNativeDeepseekEndpoint}), and the model
46
+ * the wire will carry is a documented DeepSeek vision id. The alias is
47
+ * resolved exactly the way {@link chat} resolves it — the bond's `modelMap`
48
+ * first, a miss passing through unchanged — so a host that renames the model
49
+ * on the wire (a US re-host) is priced under the id it actually serves, and a
50
+ * custom endpoint that keeps the native names is still unknown because
51
+ * another backend does the billing. Anything else — another model, another
52
+ * format, another endpoint — is an explicit unknown rather than DeepSeek's
53
+ * ceiling.
54
+ *
55
+ * @param image - The selected catalog model id and the image's media type.
56
+ * @returns 1024 tokens per image for a documented vision model and a
57
+ * supported format on DeepSeek's own endpoint, otherwise an explicit
58
+ * unknown.
59
+ */
60
+ estimateImageInputTokens(image: ImageInputSize): ImageInputTokenEstimate;
41
61
  /**
42
62
  * POST the chat body, retrying with exponential backoff on a rate-limited
43
63
  * (429) or overloaded (503) status. Extracted from {@link chat} so the
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,UAAU,EAIV,SAAS,EAET,UAAU,EAIX,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgHhD,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,QAAS,CAAA;AAgGpD;;;;GAIG;AACH,cAAM,sBAAuB,YAAW,UAAU;IAChD,QAAQ,CAAC,IAAI,cAAa;IAC1B,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,WAAW,CAAC,CAAqB;IACzC,OAAO,CAAC,aAAa,CAAC,CAAuB;IAC7C,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,sBAAsB,CAAQ;gBAE1B,MAAM,GAAE,cAAmB;IAwBvC;;;;;OAKG;IACI,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC;IAoRzD;;;;;;;;;OASG;YACW,2BAA2B;IA0EzC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,cAAc;IA8FtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB;;;;;OAKG;IACH,OAAO,CAAE,yBAAyB;IA+ElC;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IAUlB;;;;;;;;;;;;OAYG;YACY,sBAAsB;IA2GrC;;;;;;;;;;OAUG;IACH,OAAO,CAAE,eAAe;CAwOzB;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,+BAAyB,CAAA;AAExD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,UAAU,CAElE"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EACV,UAAU,EAIV,SAAS,EAET,UAAU,EAEV,cAAc,EACd,uBAAuB,EAGxB,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgHhD,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,QAAS,CAAA;AA+LpD;;;;GAIG;AACH,cAAM,sBAAuB,YAAW,UAAU;IAChD,QAAQ,CAAC,IAAI,cAAa;IAC1B,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,WAAW,CAAC,CAAqB;IACzC,OAAO,CAAC,aAAa,CAAC,CAAuB;IAC7C,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,sBAAsB,CAAQ;gBAE1B,MAAM,GAAE,cAAmB;IAwBvC;;;;;OAKG;IACI,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC;IAoRzD;;;;;;;;;;;;;;;;;;OAkBG;IACH,wBAAwB,CAAC,KAAK,EAAE,cAAc,GAAG,uBAAuB;IAQxE;;;;;;;;;OASG;YACW,2BAA2B;IA0EzC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,cAAc;IA8FtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB;;;;;OAKG;IACH,OAAO,CAAE,yBAAyB;IA+ElC;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IAUlB;;;;;;;;;;;;OAYG;YACY,sBAAsB;IA2GrC;;;;;;;;;;OAUG;IACH,OAAO,CAAE,eAAe;CAwOzB;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,+BAAyB,CAAA;AAExD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,UAAU,CAElE"}
package/dist/provider.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * @module
10
10
  */
11
11
  import { randomUUID } from 'node:crypto';
12
+ import { URL } from 'node:url';
12
13
  import { getLogger } from '@molecule/api-bond';
13
14
  import { t } from '@molecule/api-i18n';
14
15
  const logger = getLogger();
@@ -41,6 +42,101 @@ const MAX_TIMER_MS = 2_147_483_647;
41
42
  * A longer list is sent truncated rather than refused by the API.
42
43
  */
43
44
  const MAX_STOP_SEQUENCES = 16;
45
+ /**
46
+ * Model ids DeepSeek documents as accepting image input (vision guide, read
47
+ * 2026-10-25): `deepseek-flash` takes images alongside text, and the retired
48
+ * `deepseek-v4-flash-vision-exp` id is still accepted and served by the latest
49
+ * Flash model. Any other id — including one this bond's own `modelMap` sends to
50
+ * a different host — has no documented image rule, so it answers unknown.
51
+ *
52
+ * A documented id is necessary but not sufficient: the ceiling is a fact about
53
+ * DeepSeek's OWN backend, so it is applied only when the request is going to
54
+ * DeepSeek's own documented endpoint (see {@link isNativeDeepseekEndpoint}).
55
+ */
56
+ const VISION_MODEL_IDS = ['deepseek-flash', 'deepseek-v4-flash-vision-exp'];
57
+ /**
58
+ * The image formats the vision guide lists as supported: JPEG, PNG, GIF and
59
+ * WebP. A payload of any other type is not covered by the token rule.
60
+ */
61
+ const VISION_MEDIA_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
62
+ /**
63
+ * DeepSeek's documented ceiling for ONE image, in input tokens: the guide states
64
+ * "there is an upper bound of 1024 tokens per image", each image "counted
65
+ * independently under the same rule" with no separate multi-image calculation.
66
+ * Images below roughly 544×544 are scaled up and larger ones down to roughly
67
+ * 1300×1300, so the bound holds whatever the source dimensions are — including
68
+ * `detail: 'low'` (downscaled to 512×512) and `'original'`/`'auto'`.
69
+ *
70
+ * The request-level limits around it (32 MiB per base64 image, 8192 px per side,
71
+ * images in `user` messages only) reject an unusable request rather than change
72
+ * the price, so this bond prices the image alone and leaves message placement to
73
+ * the caller that knows it.
74
+ */
75
+ const VISION_TOKENS_PER_IMAGE_UPPER_BOUND = 1024;
76
+ /**
77
+ * The host DeepSeek documents as its own API endpoint. The per-image ceiling is
78
+ * a fact about THIS backend's accounting; the same model ids served by a
79
+ * gateway, a proxy or another OpenAI-compatible host are billed by that host's
80
+ * rule, which this bond cannot point at documentation for.
81
+ */
82
+ const NATIVE_ENDPOINT_HOST = 'api.deepseek.com';
83
+ /**
84
+ * The chat-completions path DeepSeek documents, in both of its published base
85
+ * URL forms: `https://api.deepseek.com` + `/chat/completions`, and
86
+ * `https://api.deepseek.com/v1` + `/chat/completions` (this bond's default is
87
+ * the root base with `/v1/chat/completions`).
88
+ */
89
+ const NATIVE_COMPLETIONS_PATHS = ['/v1/chat/completions', '/chat/completions'];
90
+ /**
91
+ * Whether the endpoint this bond will actually POST to is DeepSeek's own
92
+ * documented HTTPS endpoint.
93
+ *
94
+ * The documented per-image ceiling belongs to DeepSeek's backend, and a
95
+ * `baseUrl`, `DEEPSEEK_BASE_URL` or `completionsPath` can point this bond at a
96
+ * gateway, a proxy or another provider's OpenAI-compatible host. Model alias
97
+ * resolution alone cannot catch that: a custom endpoint may keep the native
98
+ * model names, so the wire still says `deepseek-flash` while another backend
99
+ * does the billing. Only the RESOLVED endpoint can say which backend it is.
100
+ *
101
+ * Parsed with the WHATWG URL parser, so the answer is about the endpoint as the
102
+ * request will be sent: https only, the exact hostname, no explicit
103
+ * non-default port (an explicit `:443` normalizes away, being the same
104
+ * endpoint), no credentials, no query or fragment, and a pathname equal to one
105
+ * of the two documented completions paths. Neither the parser nor the request
106
+ * normalizes duplicate or trailing slashes, so a base URL carrying one composes
107
+ * to a different path — `/v1/chat/completions/` or `//v1/chat/completions` —
108
+ * which is not native; anything else (another host, a port, a query, a
109
+ * fragment, an unparsable or relative URL) is not native either.
110
+ *
111
+ * Returns a boolean only: no part of the URL is logged, and nothing here reads,
112
+ * echoes or reports a credential.
113
+ *
114
+ * @param endpoint - The resolved request URL, `baseUrl + completionsPath`.
115
+ * @returns True only for DeepSeek's own documented endpoint.
116
+ */
117
+ function isNativeDeepseekEndpoint(endpoint) {
118
+ let url;
119
+ try {
120
+ url = new URL(endpoint);
121
+ }
122
+ catch (_error) {
123
+ // Not a URL this bond can reason about, so the documented rule does not
124
+ // apply. The caller answers unknown, which needs neither a log line nor an
125
+ // echo of the value that failed to parse.
126
+ return false;
127
+ }
128
+ if (url.protocol !== 'https:')
129
+ return false;
130
+ if (url.hostname !== NATIVE_ENDPOINT_HOST)
131
+ return false;
132
+ if (url.port !== '')
133
+ return false;
134
+ if (url.username !== '' || url.password !== '')
135
+ return false;
136
+ if (url.search !== '' || url.hash !== '')
137
+ return false;
138
+ return NATIVE_COMPLETIONS_PATHS.includes(url.pathname);
139
+ }
44
140
  /**
45
141
  * Resolve a stream bound from the config, then the environment, then the
46
142
  * default. A value that is not a positive finite number is IGNORED rather than
@@ -410,6 +506,35 @@ class DeepseekAIProviderImpl {
410
506
  }
411
507
  }
412
508
  }
509
+ /**
510
+ * DeepSeek's own documented price for one image, in input tokens.
511
+ *
512
+ * Priced only when BOTH conditions hold: the request is going to DeepSeek's
513
+ * own documented endpoint ({@link isNativeDeepseekEndpoint}), and the model
514
+ * the wire will carry is a documented DeepSeek vision id. The alias is
515
+ * resolved exactly the way {@link chat} resolves it — the bond's `modelMap`
516
+ * first, a miss passing through unchanged — so a host that renames the model
517
+ * on the wire (a US re-host) is priced under the id it actually serves, and a
518
+ * custom endpoint that keeps the native names is still unknown because
519
+ * another backend does the billing. Anything else — another model, another
520
+ * format, another endpoint — is an explicit unknown rather than DeepSeek's
521
+ * ceiling.
522
+ *
523
+ * @param image - The selected catalog model id and the image's media type.
524
+ * @returns 1024 tokens per image for a documented vision model and a
525
+ * supported format on DeepSeek's own endpoint, otherwise an explicit
526
+ * unknown.
527
+ */
528
+ estimateImageInputTokens(image) {
529
+ if (!isNativeDeepseekEndpoint(`${this.baseUrl}${this.completionsPath}`))
530
+ return { known: false };
531
+ const upstream = this.modelMap[image.model] ?? image.model;
532
+ if (!VISION_MODEL_IDS.includes(upstream))
533
+ return { known: false };
534
+ if (!VISION_MEDIA_TYPES.includes(image.mediaType.toLowerCase()))
535
+ return { known: false };
536
+ return { known: true, upperBoundTokens: VISION_TOKENS_PER_IMAGE_UPPER_BOUND };
537
+ }
413
538
  /**
414
539
  * POST the chat body, retrying with exponential backoff on a rate-limited
415
540
  * (429) or overloaded (503) status. Extracted from {@link chat} so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-deepseek",
3
- "version": "1.0.14",
3
+ "version": "1.1.0",
4
4
  "description": "DeepSeek AI provider for molecule.dev",
5
5
  "homepage": "https://www.molecule.dev/packages/api-ai-deepseek",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "directory": "packages/api/bonds/ai/deepseek"
34
34
  },
35
35
  "devDependencies": {
36
- "@molecule/api-ai": "1.3.5",
36
+ "@molecule/api-ai": "1.4.0",
37
37
  "@molecule/api-bond": "1.0.3",
38
38
  "@types/node": "26.6.5",
39
39
  "typescript": "6.0.3",