@tansr/api-client 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tansr (tansr.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @tansr/api-client
2
+
3
+ Tansr 网关薄客户端:用平台 key 直连 Tansr 数据面(OpenAI / Anthropic / OpenAI
4
+ Responses 三协议形)与账面(模型目录 / 用量)。**零依赖**——只用运行时全局
5
+ `fetch` 与 Web Streams,不引 openai/anthropic SDK。
6
+
7
+ - 运行环境:Node ≥ 20.3(内建 fetch),或任何具备 `fetch`/`ReadableStream` 的
8
+ 现代浏览器 / 边缘运行时;
9
+ - 请求体 passthrough:网关不做协议转换,字段语义与各上游协议一致,本包类型
10
+ 只钉常用字段、其余原样放行。
11
+
12
+ ## 获取 key
13
+
14
+ 登录 Tansr 门户,在 **`/console` → API Keys** 创建 PAT(`tansr_sk_` 前缀;scope
15
+ 可选 `data` 或 `data control`——`/v1/usage` 需要 `control`)。登录态 access JWT
16
+ 同样可用,两者均走 `Authorization: Bearer`。
17
+
18
+ ## 三行起步
19
+
20
+ ```ts
21
+ import { Tansr } from '@tansr/api-client';
22
+
23
+ const client = new Tansr({ apiKey: process.env.TANSR_API_KEY }); // 省略 apiKey 时自动读 env TANSR_API_KEY
24
+ const r = await client.chat.completions.create({
25
+ model: 'kimi-k3',
26
+ messages: [{ role: 'user', content: '你好' }],
27
+ });
28
+ console.log(r.choices?.[0]?.message?.content);
29
+ console.log(r.meta); // { requestId, cost, balance } —— 消费 x-tansr-* 响应头
30
+ ```
31
+
32
+ 自建/私有部署传 `baseUrl`(含 `/v1`):`new Tansr({ apiKey, baseUrl: 'http://127.0.0.1:8787/v1' })`。
33
+
34
+ ## 流式(SSE)
35
+
36
+ ```ts
37
+ const stream = client.chat.completions.stream({ model: 'kimi-k3', messages });
38
+ for await (const chunk of stream) {
39
+ process.stdout.write(chunk.choices?.[0]?.delta?.content ?? '');
40
+ }
41
+ console.log(stream.meta); // 迭代完成后可取;流式无 x-tansr-cost(成本流末才可知),cost 恒 null
42
+ ```
43
+
44
+ - OpenAI 系(chat / responses):`data: [DONE]` 哨兵终止;
45
+ - Anthropic(messages):`event:` 行语义已归一到帧对象的 `type` 字段,流体自然
46
+ 结束即止;
47
+ - 提前 `break` 会取消上游读,网关按断链结算,不留悬挂请求;
48
+ - 流对象单次可迭代。
49
+
50
+ ## 三协议同型 API 面
51
+
52
+ | 径 | 非流式 | 流式 |
53
+ |----|--------|------|
54
+ | OpenAI chat | `client.chat.completions.create(params)` | `client.chat.completions.stream(params)` |
55
+ | Anthropic messages | `client.messages.create(params)` | `client.messages.stream(params)` |
56
+ | OpenAI Responses | `client.responses.create(params)` | `client.responses.stream(params)` |
57
+
58
+ 目录与账面:
59
+
60
+ ```ts
61
+ const models = await client.models.list();
62
+ const usage = await client.usage.get({ window: '7d', groupBy: 'model' }); // 需 control scope
63
+ ```
64
+
65
+ ## 错误处理
66
+
67
+ ```ts
68
+ import { TansrApiError, TansrConnectionError } from '@tansr/api-client';
69
+
70
+ try {
71
+ await client.chat.completions.create({ model, messages });
72
+ } catch (err) {
73
+ if (err instanceof TansrApiError) {
74
+ // 网关错误信封 { error: { code, message, requestId, detail? } } 的映射
75
+ console.error(err.status, err.code, err.requestId, err.detail);
76
+ if (err.status === 402) {/* 余额不足:detail 携 balance / estimatedCost */}
77
+ if (err.status === 429) {/* 限流:err.retryAfterMs 由 Retry-After 头换算(服务端原值,毫秒) */}
78
+ } else if (err instanceof TansrConnectionError) {
79
+ // 网络故障 / 超时(cause 保真底层错误)
80
+ }
81
+ }
82
+ ```
83
+
84
+ 上游 4xx/5xx 由网关原样透传,本包尽力从透传体提取 `code`/`message`,兜底
85
+ `http_<status>`。错误对象永不携带 api key。
86
+
87
+ ## 取消与超时
88
+
89
+ ```ts
90
+ const ac = new AbortController();
91
+ const p = client.messages.create(params, { signal: ac.signal, timeoutMs: 30_000 });
92
+ ac.abort(); // 用户取消:原样抛调用方的 AbortError,不做包装
93
+ ```
94
+
95
+ `timeoutMs`(构造器缺省 600_000)是**一次调用的总预算**:以调用起点计,覆盖整个
96
+ 请求生命周期(含流式全程),也含幂等 GET 的 429 重试等待与后续尝试——重试不会
97
+ 重新起算。预算耗尽抛 `TansrConnectionError`(`timed out after <timeoutMs>ms`)。
98
+
99
+ ## 重试纪律
100
+
101
+ - 幂等 GET(`models.list` / `usage.get`)的 429 自动重试:尊重 `Retry-After`
102
+ (秒、小数秒或 HTTP-date;缺失 / 不可解析时回落 1 s、2 s 退避),上限 2 次;
103
+ - 单次等待钳 **30 s** 上限(`Retry-After` 来自对端,不让远端决定客户端悬挂时长);
104
+ - 等待与重试都在 `timeoutMs` 总预算之内:剩余预算盖不住(钳制后的)等待时**不再等待、
105
+ 不再重试**,直接抛出该 429 的 `TansrApiError`——`status === 429`,`retryAfterMs`
106
+ 携服务端原值(不受 30 s 钳制影响),调用方可据此自行排程:
107
+
108
+ ```ts
109
+ try {
110
+ await client.models.list({ timeoutMs: 5_000 });
111
+ } catch (err) {
112
+ if (err instanceof TansrApiError && err.status === 429 && err.retryAfterMs !== null) {
113
+ // 预算内放弃重试:按服务端建议自行排程,而不是让本次调用悬挂
114
+ scheduleRetry(err.retryAfterMs);
115
+ }
116
+ }
117
+ ```
118
+
119
+ - **POST 数据面一律不自动重试**:网关按请求预扣与结算(计费面),盲重试会
120
+ 重复计费——失败原样抛出,由调用方决策。
121
+
122
+ ## meta 挂载说明
123
+
124
+ 非流式返回值的 `meta` 是**不可枚举**属性:`JSON.stringify(响应)` 不含 meta,
125
+ 持久化原始响应体不被污染;注意 `{ ...res }` 展开会丢弃它,先取后展开。
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Tansr 网关薄客户端入口:三行起步——
3
+ *
4
+ * const client = new Tansr({ apiKey: process.env.TANSR_API_KEY });
5
+ * const r = await client.chat.completions.create({ model, messages });
6
+ *
7
+ * 设计:零依赖(仅全局 fetch / Web Streams,不引 openai/anthropic SDK,不依赖
8
+ * 仓内 kernel/providers/protocol);数据面请求体原样直达网关(passthrough,
9
+ * 不做协议转换),三协议(chat/messages/responses)各走各径。api key 只存
10
+ * 私有字段并落 authorization 头,客户端与错误对象的序列化面零泄漏。
11
+ */
12
+ import { HttpCore } from './http.js';
13
+ import { TansrStream } from './stream.js';
14
+ import type { AnthropicMessage, ChatCompletion, ChatCompletionChunk, ChatCompletionsCreateParams, MessageStreamEvent, MessagesCreateParams, ModelsListResponse, RequestOptions, ResponseObject, ResponseStreamEvent, ResponsesCreateParams, UsageGetParams, UsageReport, WithMeta } from './types.js';
15
+ export interface TansrOptions {
16
+ /** 平台 key:PAT(`tansr_sk_` 前缀)或登录 access JWT,均走 Bearer;缺省回落 env TANSR_API_KEY。 */
17
+ readonly apiKey?: string;
18
+ /** 网关基址(含 /v1;尾斜杠自动剥除),缺省 https://api.tansr.com/v1。 */
19
+ readonly baseUrl?: string;
20
+ /**
21
+ * 单次调用的总预算(毫秒,以调用起点计):覆盖流式全程,也含幂等 GET 的 429 重试等待与
22
+ * 后续尝试;剩余预算盖不住 Retry-After(钳 30 s)时不再等待,直接抛 429 TansrApiError
23
+ * (携 retryAfterMs)。缺省 600_000。
24
+ */
25
+ readonly timeoutMs?: number;
26
+ /** 自定义 fetch(测试注入缝;沿仓惯例命名 fetchImpl),缺省全局 fetch。 */
27
+ readonly fetchImpl?: typeof fetch;
28
+ /** 逐请求附加头(如 org 作用域 x-tansr-org);authorization/content-type/accept 不可覆盖。 */
29
+ readonly defaultHeaders?: Readonly<Record<string, string>>;
30
+ }
31
+ /** OpenAI chat completions 形数据面(POST /v1/chat/completions)。 */
32
+ export declare class ChatCompletions {
33
+ #private;
34
+ constructor(core: HttpCore);
35
+ /** 非流式;返回值挂 meta { requestId, cost, balance }(消费响应头)。 */
36
+ create(params: ChatCompletionsCreateParams, options?: RequestOptions): Promise<WithMeta<ChatCompletion>>;
37
+ /** SSE 流式(AsyncIterable 逐帧;`data: [DONE]` 终止;stream.meta 迭代完成后可取)。 */
38
+ stream(params: ChatCompletionsCreateParams, options?: RequestOptions): TansrStream<ChatCompletionChunk>;
39
+ }
40
+ /** Anthropic messages 形数据面(POST /v1/messages)。 */
41
+ export declare class Messages {
42
+ #private;
43
+ constructor(core: HttpCore);
44
+ create(params: MessagesCreateParams, options?: RequestOptions): Promise<WithMeta<AnthropicMessage>>;
45
+ /** SSE 流式(`event:` 行语义归一到 type;流体自然结束即止)。 */
46
+ stream(params: MessagesCreateParams, options?: RequestOptions): TansrStream<MessageStreamEvent>;
47
+ }
48
+ /** OpenAI Responses 形数据面(POST /v1/responses)。 */
49
+ export declare class Responses {
50
+ #private;
51
+ constructor(core: HttpCore);
52
+ create(params: ResponsesCreateParams, options?: RequestOptions): Promise<WithMeta<ResponseObject>>;
53
+ stream(params: ResponsesCreateParams, options?: RequestOptions): TansrStream<ResponseStreamEvent>;
54
+ }
55
+ /** 模型目录(GET /v1/models;幂等 GET,429 自动重试至多 2 次)。 */
56
+ export declare class Models {
57
+ #private;
58
+ constructor(core: HttpCore);
59
+ list(options?: RequestOptions): Promise<WithMeta<ModelsListResponse>>;
60
+ }
61
+ /** 用量账面(GET /v1/usage;需 key 具 control scope)。 */
62
+ export declare class Usage {
63
+ #private;
64
+ constructor(core: HttpCore);
65
+ get(params?: UsageGetParams, options?: RequestOptions): Promise<WithMeta<UsageReport>>;
66
+ }
67
+ export declare class Tansr {
68
+ readonly chat: {
69
+ readonly completions: ChatCompletions;
70
+ };
71
+ readonly messages: Messages;
72
+ readonly responses: Responses;
73
+ readonly models: Models;
74
+ readonly usage: Usage;
75
+ /** 生效的网关基址(尾斜杠已剥除)。 */
76
+ readonly baseUrl: string;
77
+ constructor(options?: TansrOptions);
78
+ }
package/dist/client.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Tansr 网关薄客户端入口:三行起步——
3
+ *
4
+ * const client = new Tansr({ apiKey: process.env.TANSR_API_KEY });
5
+ * const r = await client.chat.completions.create({ model, messages });
6
+ *
7
+ * 设计:零依赖(仅全局 fetch / Web Streams,不引 openai/anthropic SDK,不依赖
8
+ * 仓内 kernel/providers/protocol);数据面请求体原样直达网关(passthrough,
9
+ * 不做协议转换),三协议(chat/messages/responses)各走各径。api key 只存
10
+ * 私有字段并落 authorization 头,客户端与错误对象的序列化面零泄漏。
11
+ */
12
+ import { HttpCore } from './http.js';
13
+ import { TansrStream } from './stream.js';
14
+ import { TansrConnectionError } from './errors.js';
15
+ const DEFAULT_BASE_URL = 'https://api.tansr.com/v1';
16
+ const DEFAULT_TIMEOUT_MS = 600_000;
17
+ /** OpenAI chat completions 形数据面(POST /v1/chat/completions)。 */
18
+ export class ChatCompletions {
19
+ #core;
20
+ constructor(core) {
21
+ this.#core = core;
22
+ }
23
+ /** 非流式;返回值挂 meta { requestId, cost, balance }(消费响应头)。 */
24
+ async create(params, options) {
25
+ assertNotStreamParams(params, 'chat.completions');
26
+ const { json, meta } = await this.#core.requestJson('POST', '/chat/completions', { body: params }, perRequest(options));
27
+ return withMeta(json, meta);
28
+ }
29
+ /** SSE 流式(AsyncIterable 逐帧;`data: [DONE]` 终止;stream.meta 迭代完成后可取)。 */
30
+ stream(params, options) {
31
+ return new TansrStream(() => this.#core.openStream('/chat/completions', { ...params, stream: true }, perRequest(options)));
32
+ }
33
+ }
34
+ /** Anthropic messages 形数据面(POST /v1/messages)。 */
35
+ export class Messages {
36
+ #core;
37
+ constructor(core) {
38
+ this.#core = core;
39
+ }
40
+ async create(params, options) {
41
+ assertNotStreamParams(params, 'messages');
42
+ const { json, meta } = await this.#core.requestJson('POST', '/messages', { body: params }, perRequest(options));
43
+ return withMeta(json, meta);
44
+ }
45
+ /** SSE 流式(`event:` 行语义归一到 type;流体自然结束即止)。 */
46
+ stream(params, options) {
47
+ return new TansrStream(() => this.#core.openStream('/messages', { ...params, stream: true }, perRequest(options)));
48
+ }
49
+ }
50
+ /** OpenAI Responses 形数据面(POST /v1/responses)。 */
51
+ export class Responses {
52
+ #core;
53
+ constructor(core) {
54
+ this.#core = core;
55
+ }
56
+ async create(params, options) {
57
+ assertNotStreamParams(params, 'responses');
58
+ const { json, meta } = await this.#core.requestJson('POST', '/responses', { body: params }, perRequest(options));
59
+ return withMeta(json, meta);
60
+ }
61
+ stream(params, options) {
62
+ return new TansrStream(() => this.#core.openStream('/responses', { ...params, stream: true }, perRequest(options)));
63
+ }
64
+ }
65
+ /** 模型目录(GET /v1/models;幂等 GET,429 自动重试至多 2 次)。 */
66
+ export class Models {
67
+ #core;
68
+ constructor(core) {
69
+ this.#core = core;
70
+ }
71
+ async list(options) {
72
+ const { json, meta } = await this.#core.requestJson('GET', '/models', {}, perRequest(options));
73
+ return withMeta(json, meta);
74
+ }
75
+ }
76
+ /** 用量账面(GET /v1/usage;需 key 具 control scope)。 */
77
+ export class Usage {
78
+ #core;
79
+ constructor(core) {
80
+ this.#core = core;
81
+ }
82
+ async get(params = {}, options) {
83
+ const groupBy = params.groupBy === undefined
84
+ ? undefined
85
+ : typeof params.groupBy === 'string'
86
+ ? params.groupBy
87
+ : params.groupBy.join(',');
88
+ const query = { window: params.window, groupBy };
89
+ const { json, meta } = await this.#core.requestJson('GET', '/usage', { query }, perRequest(options));
90
+ return withMeta(json, meta);
91
+ }
92
+ }
93
+ export class Tansr {
94
+ chat;
95
+ messages;
96
+ responses;
97
+ models;
98
+ usage;
99
+ /** 生效的网关基址(尾斜杠已剥除)。 */
100
+ baseUrl;
101
+ constructor(options = {}) {
102
+ const apiKey = options.apiKey ?? envApiKey();
103
+ if (typeof apiKey !== 'string' || apiKey === '') {
104
+ // 消息只提示来源,永不回显任何 key 材料
105
+ throw new TypeError('tansr api key is required: pass { apiKey } or set the TANSR_API_KEY env var');
106
+ }
107
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
108
+ if (typeof fetchImpl !== 'function') {
109
+ throw new TypeError('global fetch is unavailable: pass { fetchImpl } (Node >= 20.3 has fetch built in)');
110
+ }
111
+ const core = new HttpCore({
112
+ apiKey,
113
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
114
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
115
+ fetchImpl,
116
+ defaultHeaders: options.defaultHeaders ?? {},
117
+ });
118
+ this.baseUrl = core.baseUrl;
119
+ this.chat = { completions: new ChatCompletions(core) };
120
+ this.messages = new Messages(core);
121
+ this.responses = new Responses(core);
122
+ this.models = new Models(core);
123
+ this.usage = new Usage(core);
124
+ }
125
+ }
126
+ /** 浏览器/边缘运行时无 process:安全探测,不硬依赖 node 类型面。 */
127
+ function envApiKey() {
128
+ const proc = globalThis.process;
129
+ return proc?.env?.TANSR_API_KEY;
130
+ }
131
+ function perRequest(options) {
132
+ return { signal: options?.signal, timeoutMs: options?.timeoutMs };
133
+ }
134
+ /** create 径拒收 stream: true——流式必须走 .stream()(返回形态不同,静默降级会误导)。 */
135
+ function assertNotStreamParams(params, label) {
136
+ if (params.stream === true) {
137
+ throw new TypeError(`${label}.create() does not stream; use ${label}.stream(...) instead`);
138
+ }
139
+ }
140
+ /**
141
+ * meta 以不可枚举属性挂载:JSON.stringify(响应) 不含 meta,原始响应体持久化
142
+ * 不被污染(注意 { ...res } 展开会丢弃 meta,先取后展开)。
143
+ */
144
+ function withMeta(value, meta) {
145
+ if (typeof value !== 'object' || value === null) {
146
+ throw new TansrConnectionError('gateway returned a non-object JSON body');
147
+ }
148
+ Object.defineProperty(value, 'meta', { value: meta, enumerable: false, configurable: true });
149
+ return value;
150
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @tansr/api-client 错误面。
3
+ *
4
+ * 两类错误:
5
+ * - TansrApiError:网关给出了 HTTP 错误响应——tansr 错误信封
6
+ * `{ error: { code, message, requestId, detail? } }`,或上游 4xx/5xx 透传体
7
+ * (网关不造方言,原样转发,本客户端尽力提取 code/message)。
8
+ * 402 余额不足 / 429 限流(带 retryAfterMs)均为其实例。
9
+ * - TansrConnectionError:请求根本没到达或没读完(网络故障、超时、响应体
10
+ * 非法 JSON)。用户主动 AbortSignal 取消不属于两者——原样上抛调用方的
11
+ * 中止异常(AbortError),不做包装。
12
+ *
13
+ * 纪律:错误对象只携带服务端返回的信息与状态码,构造参数里没有、也永不
14
+ * 引用 api key / authorization 头(零泄漏;测试对序列化面有专项断言)。
15
+ */
16
+ export interface TansrApiErrorArgs {
17
+ readonly status: number;
18
+ readonly code: string;
19
+ readonly message: string;
20
+ readonly requestId?: string | null;
21
+ readonly detail?: Record<string, unknown> | null;
22
+ readonly retryAfterMs?: number | null;
23
+ }
24
+ /** 网关 HTTP 错误(错误信封映射;402/429 等均为本类实例,按 status/code 分派)。 */
25
+ export declare class TansrApiError extends Error {
26
+ /** HTTP 状态码(402 余额不足、429 限流、4xx/5xx 上游透传等)。 */
27
+ readonly status: number;
28
+ /** 稳定错误码(tansr 信封 code;上游透传时尽力取 code/type,兜底 `http_<status>`)。 */
29
+ readonly code: string;
30
+ /** 服务端请求标识(信封 requestId,缺省回落 x-tansr-request-id 头),用于回执排障。 */
31
+ readonly requestId: string | null;
32
+ /** 信封附加细节(如 402 的 { balance, estimatedCost });上游透传体无此位。 */
33
+ readonly detail: Record<string, unknown> | null;
34
+ /**
35
+ * 429 时由 Retry-After 头(秒 / HTTP-date)/ 信封 detail.retryAfterSec 换算成毫秒;其余情形为 null。
36
+ * 恒为服务端原值:客户端自动重试的等待另有 30 s 钳制与 timeoutMs 总预算,不改写此字段——
37
+ * 预算内放弃重试时调用方仍据此自行排程。
38
+ */
39
+ readonly retryAfterMs: number | null;
40
+ constructor(args: TansrApiErrorArgs);
41
+ }
42
+ /** 网络层故障:连不上网关 / 超时 / 响应体读取或解析失败(cause 保真底层错误)。 */
43
+ export declare class TansrConnectionError extends Error {
44
+ constructor(message: string, options?: {
45
+ cause?: unknown;
46
+ });
47
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @tansr/api-client 错误面。
3
+ *
4
+ * 两类错误:
5
+ * - TansrApiError:网关给出了 HTTP 错误响应——tansr 错误信封
6
+ * `{ error: { code, message, requestId, detail? } }`,或上游 4xx/5xx 透传体
7
+ * (网关不造方言,原样转发,本客户端尽力提取 code/message)。
8
+ * 402 余额不足 / 429 限流(带 retryAfterMs)均为其实例。
9
+ * - TansrConnectionError:请求根本没到达或没读完(网络故障、超时、响应体
10
+ * 非法 JSON)。用户主动 AbortSignal 取消不属于两者——原样上抛调用方的
11
+ * 中止异常(AbortError),不做包装。
12
+ *
13
+ * 纪律:错误对象只携带服务端返回的信息与状态码,构造参数里没有、也永不
14
+ * 引用 api key / authorization 头(零泄漏;测试对序列化面有专项断言)。
15
+ */
16
+ /** 网关 HTTP 错误(错误信封映射;402/429 等均为本类实例,按 status/code 分派)。 */
17
+ export class TansrApiError extends Error {
18
+ /** HTTP 状态码(402 余额不足、429 限流、4xx/5xx 上游透传等)。 */
19
+ status;
20
+ /** 稳定错误码(tansr 信封 code;上游透传时尽力取 code/type,兜底 `http_<status>`)。 */
21
+ code;
22
+ /** 服务端请求标识(信封 requestId,缺省回落 x-tansr-request-id 头),用于回执排障。 */
23
+ requestId;
24
+ /** 信封附加细节(如 402 的 { balance, estimatedCost });上游透传体无此位。 */
25
+ detail;
26
+ /**
27
+ * 429 时由 Retry-After 头(秒 / HTTP-date)/ 信封 detail.retryAfterSec 换算成毫秒;其余情形为 null。
28
+ * 恒为服务端原值:客户端自动重试的等待另有 30 s 钳制与 timeoutMs 总预算,不改写此字段——
29
+ * 预算内放弃重试时调用方仍据此自行排程。
30
+ */
31
+ retryAfterMs;
32
+ constructor(args) {
33
+ super(args.message);
34
+ this.name = 'TansrApiError';
35
+ this.status = args.status;
36
+ this.code = args.code;
37
+ this.requestId = args.requestId ?? null;
38
+ this.detail = args.detail ?? null;
39
+ this.retryAfterMs = args.retryAfterMs ?? null;
40
+ }
41
+ }
42
+ /** 网络层故障:连不上网关 / 超时 / 响应体读取或解析失败(cause 保真底层错误)。 */
43
+ export class TansrConnectionError extends Error {
44
+ constructor(message, options) {
45
+ super(message, options);
46
+ this.name = 'TansrConnectionError';
47
+ }
48
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { ResponseMeta } from './types.js';
2
+ export declare const HEADER_REQUEST_ID = "x-tansr-request-id";
3
+ export declare const HEADER_COST = "x-tansr-cost";
4
+ export declare const HEADER_BALANCE = "x-tansr-balance";
5
+ /**
6
+ * 计时缝(包内私有,不经 index.ts 导出):产品缺省用运行时全局计时器;测试注入虚拟时钟,
7
+ * 以确定性推进验证总预算 / 钳制等时序语义,不靠真实等待。
8
+ */
9
+ export interface HttpClock {
10
+ readonly now: () => number;
11
+ readonly setTimeout: (callback: () => void, delayMs: number) => unknown;
12
+ readonly clearTimeout: (handle: unknown) => void;
13
+ }
14
+ export interface HttpCoreOptions {
15
+ readonly apiKey: string;
16
+ readonly baseUrl: string;
17
+ readonly timeoutMs: number;
18
+ readonly fetchImpl: typeof fetch;
19
+ readonly defaultHeaders: Readonly<Record<string, string>>;
20
+ /** 计时缝(测试注入;缺省全局计时器)。 */
21
+ readonly clock?: HttpClock | undefined;
22
+ }
23
+ export interface PerRequestOptions {
24
+ readonly signal?: AbortSignal | undefined;
25
+ readonly timeoutMs?: number | undefined;
26
+ }
27
+ /** openStream 返回的流句柄:响应 + meta + 停表/中断归因钩子(供 TansrStream 消费)。 */
28
+ export interface StreamHandle {
29
+ readonly response: Response;
30
+ readonly meta: ResponseMeta;
31
+ /** 流读毕/中止后停表(超时计时器覆盖流全程,必须成对调用)。 */
32
+ readonly finish: () => void;
33
+ /** 流中断归因:超时 → TansrConnectionError;用户取消 → 原样返回;其余包装网络错误。 */
34
+ readonly mapStreamError: (err: unknown) => unknown;
35
+ }
36
+ export declare function metaFromHeaders(headers: Headers): ResponseMeta;
37
+ export declare class HttpCore {
38
+ #private;
39
+ readonly baseUrl: string;
40
+ constructor(opts: HttpCoreOptions);
41
+ /**
42
+ * 非流式 JSON 请求(GET 走 429 重试纪律;2xx 断言对象体并回 meta)。
43
+ * 整个循环(每次 fetch + 体读取 + 重试等待)共用一个以调用起点计的总预算。
44
+ */
45
+ requestJson(method: 'GET' | 'POST', path: string, args: {
46
+ readonly query?: Record<string, string | undefined>;
47
+ readonly body?: unknown;
48
+ }, opts?: PerRequestOptions): Promise<{
49
+ json: Record<string, unknown>;
50
+ meta: ResponseMeta;
51
+ }>;
52
+ /**
53
+ * 流式请求(POST,accept: text/event-stream):零重试(计费面,见头注)。
54
+ * 非 2xx 在此按错误信封抛出;2xx 交还句柄由 TansrStream 逐帧消费。
55
+ */
56
+ openStream(path: string, body: unknown, opts?: PerRequestOptions): Promise<StreamHandle>;
57
+ }