llm-simple-router 0.7.1 → 0.8.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.
Files changed (153) hide show
  1. package/dist/admin/proxy-enhancement.js +3 -1
  2. package/dist/admin/routes.d.ts +1 -0
  3. package/dist/admin/routes.js +3 -1
  4. package/dist/admin/settings-import-export.d.ts +1 -0
  5. package/dist/admin/settings-import-export.js +7 -0
  6. package/dist/admin/transform-rules.d.ts +8 -0
  7. package/dist/admin/transform-rules.js +38 -0
  8. package/dist/admin/usage.js +1 -1
  9. package/dist/core/container.d.ts +1 -0
  10. package/dist/core/container.js +1 -0
  11. package/dist/db/migrations/034_create_provider_transform_rules.sql +11 -0
  12. package/dist/db/transform-rules.d.ts +16 -0
  13. package/dist/db/transform-rules.js +51 -0
  14. package/dist/index.js +30 -1
  15. package/dist/metrics/sse-parser.d.ts +2 -0
  16. package/dist/metrics/sse-parser.js +4 -0
  17. package/dist/monitor/request-tracker.d.ts +2 -0
  18. package/dist/monitor/request-tracker.js +22 -1
  19. package/dist/monitor/types.d.ts +1 -1
  20. package/dist/proxy/enhancement/response-cleaner.js +14 -6
  21. package/dist/proxy/handler/openai.js +13 -4
  22. package/dist/proxy/handler/proxy-handler-utils.js +2 -7
  23. package/dist/proxy/handler/proxy-handler.js +85 -18
  24. package/dist/proxy/patch/deepseek/index.d.ts +15 -3
  25. package/dist/proxy/patch/deepseek/index.js +29 -6
  26. package/dist/proxy/patch/deepseek/patch-cache-control.d.ts +6 -0
  27. package/dist/proxy/patch/deepseek/patch-cache-control.js +30 -0
  28. package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.d.ts +16 -0
  29. package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.js +74 -0
  30. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +10 -1
  31. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +58 -15
  32. package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +5 -1
  33. package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +37 -4
  34. package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +6 -0
  35. package/dist/proxy/patch/deepseek/patch-thinking-param.js +32 -0
  36. package/dist/proxy/patch/deepseek/utils.d.ts +8 -0
  37. package/dist/proxy/patch/deepseek/utils.js +38 -0
  38. package/dist/proxy/patch/index.d.ts +2 -2
  39. package/dist/proxy/patch/index.js +50 -4
  40. package/dist/proxy/patch/router-cleanup.js +1 -24
  41. package/dist/proxy/patch/safe-sse-parser.d.ts +9 -0
  42. package/dist/proxy/patch/safe-sse-parser.js +16 -0
  43. package/dist/proxy/patch/tool-round-limiter.d.ts +38 -0
  44. package/dist/proxy/patch/tool-round-limiter.js +115 -0
  45. package/dist/proxy/pipeline-snapshot.d.ts +4 -0
  46. package/dist/proxy/proxy-core.js +1 -0
  47. package/dist/proxy/proxy-logging.d.ts +1 -1
  48. package/dist/proxy/proxy-logging.js +3 -3
  49. package/dist/proxy/routing/enhancement-config.d.ts +1 -0
  50. package/dist/proxy/routing/enhancement-config.js +2 -0
  51. package/dist/proxy/transform/id-utils.d.ts +3 -0
  52. package/dist/proxy/transform/id-utils.js +9 -0
  53. package/dist/proxy/transform/message-mapper.d.ts +15 -0
  54. package/dist/proxy/transform/message-mapper.js +173 -0
  55. package/dist/proxy/transform/plugin-registry.d.ts +23 -0
  56. package/dist/proxy/transform/plugin-registry.js +130 -0
  57. package/dist/proxy/transform/plugin-types.d.ts +46 -0
  58. package/dist/proxy/transform/plugin-types.js +15 -0
  59. package/dist/proxy/transform/provider-meta.d.ts +29 -0
  60. package/dist/proxy/transform/provider-meta.js +72 -0
  61. package/dist/proxy/transform/request-transform.d.ts +4 -0
  62. package/dist/proxy/transform/request-transform.js +151 -0
  63. package/dist/proxy/transform/response-transform.d.ts +4 -0
  64. package/dist/proxy/transform/response-transform.js +99 -0
  65. package/dist/proxy/transform/sanitize.d.ts +3 -0
  66. package/dist/proxy/transform/sanitize.js +24 -0
  67. package/dist/proxy/transform/stream-ant2oa.d.ts +20 -0
  68. package/dist/proxy/transform/stream-ant2oa.js +200 -0
  69. package/dist/proxy/transform/stream-oa2ant.d.ts +25 -0
  70. package/dist/proxy/transform/stream-oa2ant.js +201 -0
  71. package/dist/proxy/transform/stream-transform-base.d.ts +19 -0
  72. package/dist/proxy/transform/stream-transform-base.js +61 -0
  73. package/dist/proxy/transform/thinking-mapper.d.ts +4 -0
  74. package/dist/proxy/transform/thinking-mapper.js +15 -0
  75. package/dist/proxy/transform/tool-mapper.d.ts +8 -0
  76. package/dist/proxy/transform/tool-mapper.js +67 -0
  77. package/dist/proxy/transform/transform-coordinator.d.ts +11 -0
  78. package/dist/proxy/transform/transform-coordinator.js +32 -0
  79. package/dist/proxy/transform/types.d.ts +43 -0
  80. package/dist/proxy/transform/types.js +1 -0
  81. package/dist/proxy/transform/usage-mapper.d.ts +8 -0
  82. package/dist/proxy/transform/usage-mapper.js +46 -0
  83. package/dist/proxy/transport/stream.d.ts +1 -1
  84. package/dist/proxy/transport/stream.js +19 -10
  85. package/dist/proxy/transport/transport-fn.d.ts +3 -0
  86. package/dist/proxy/transport/transport-fn.js +11 -4
  87. package/dist/storage/log-file-compressor.js +5 -6
  88. package/dist/storage/log-file-writer.js +11 -13
  89. package/dist/storage/types.d.ts +2 -0
  90. package/dist/storage/types.js +7 -0
  91. package/frontend-dist/assets/{CardContent-CxOF1feY.js → CardContent-BVMQ2_pg.js} +1 -1
  92. package/frontend-dist/assets/{CardTitle-BSEFcEOM.js → CardTitle-GLv7QyIY.js} +1 -1
  93. package/frontend-dist/assets/{CascadingModelSelect-DTwksDPZ.js → CascadingModelSelect-CBhqKFDX.js} +1 -1
  94. package/frontend-dist/assets/{Checkbox-RfsERG07.js → Checkbox-HPVDmEdV.js} +1 -1
  95. package/frontend-dist/assets/{CollapsibleTrigger-Dsjo7QlC.js → CollapsibleTrigger-DhxD9tpM.js} +1 -1
  96. package/frontend-dist/assets/{Collection-rQ4eIYfa.js → Collection-BRt7YxN8.js} +1 -1
  97. package/frontend-dist/assets/{Dashboard-YejfAPiB.js → Dashboard-D1Ys8Zog.js} +1 -1
  98. package/frontend-dist/assets/{DialogTitle-DeFTnmgC.js → DialogTitle-23q73lwF.js} +1 -1
  99. package/frontend-dist/assets/{Input-CENz_g9t.js → Input-CAnKUBBK.js} +1 -1
  100. package/frontend-dist/assets/{Label-BAciBrrd.js → Label-DWdYtVMI.js} +1 -1
  101. package/frontend-dist/assets/{Login-DQkYFq7R.js → Login-w5WFOinP.js} +1 -1
  102. package/frontend-dist/assets/{Logs-Dol8AX7z.js → Logs-C1F1ZmWF.js} +1 -1
  103. package/frontend-dist/assets/{ModelMappings-VEYW1TrW.js → ModelMappings-BzmecWEH.js} +1 -1
  104. package/frontend-dist/assets/{Monitor-C0r9WefB.js → Monitor-DrAZFTKR.js} +1 -1
  105. package/frontend-dist/assets/{PopoverTrigger-Cyqik5SE.js → PopoverTrigger-Bj65uUbv.js} +1 -1
  106. package/frontend-dist/assets/{PopperContent-B7IuAHeq.js → PopperContent-gzzf1XHe.js} +1 -1
  107. package/frontend-dist/assets/Providers-DSgf4mb6.js +1 -0
  108. package/frontend-dist/assets/ProxyEnhancement-Bb1cCP6d.js +5 -0
  109. package/frontend-dist/assets/{RetryRules-F0295m4_.js → RetryRules-BwPfEZtm.js} +1 -1
  110. package/frontend-dist/assets/{RouterKeys-CFbPtUE_.js → RouterKeys-CzTSq1Mx.js} +1 -1
  111. package/frontend-dist/assets/{RovingFocusItem-D291Vjh8.js → RovingFocusItem-CXM_Yfkm.js} +1 -1
  112. package/frontend-dist/assets/{Schedules-DWhF3uod.js → Schedules-DVilCXrC.js} +1 -1
  113. package/frontend-dist/assets/{SelectValue-BWlgUZa3.js → SelectValue-C0-LzGQY.js} +1 -1
  114. package/frontend-dist/assets/{Settings-BnIzEF_k.js → Settings-Bpk53zVX.js} +1 -1
  115. package/frontend-dist/assets/{Setup-BglKyQKq.js → Setup-Dn7EgC49.js} +1 -1
  116. package/frontend-dist/assets/{Switch-DyCR-CPu.js → Switch-BO8Ooae6.js} +1 -1
  117. package/frontend-dist/assets/{TableHeader-DVUlBL35.js → TableHeader-Bded9VTC.js} +1 -1
  118. package/frontend-dist/assets/{TabsTrigger-BU1DY-C8.js → TabsTrigger-BzKMi9AF.js} +1 -1
  119. package/frontend-dist/assets/{Teleport-BQgusr9g.js → Teleport-DizRK5O3.js} +1 -1
  120. package/frontend-dist/assets/{TooltipTrigger-Bv_QoBns.js → TooltipTrigger-EiIy2zn8.js} +1 -1
  121. package/frontend-dist/assets/{UnifiedRequestDialog-f_evI835.js → UnifiedRequestDialog-BABsTaGb.js} +1 -1
  122. package/frontend-dist/assets/{VisuallyHidden-Con10z4F.js → VisuallyHidden-5AozJQza.js} +1 -1
  123. package/frontend-dist/assets/{VisuallyHiddenInput-yrDtxucb.js → VisuallyHiddenInput-DdiZrV2i.js} +1 -1
  124. package/frontend-dist/assets/{alert-dialog-2Db6Z7JQ.js → alert-dialog-DlKUuTPe.js} +1 -1
  125. package/frontend-dist/assets/arrow-down-CxWKmZ2I.js +1 -0
  126. package/frontend-dist/assets/{badge-DEhZfeI0.js → badge-9KJEMa53.js} +1 -1
  127. package/frontend-dist/assets/button-Ul8WlrM5.js +12 -0
  128. package/frontend-dist/assets/check-7ahK--N4.js +1 -0
  129. package/frontend-dist/assets/{copy-CwqZSuIG.js → copy-DzU2pAMG.js} +1 -1
  130. package/frontend-dist/assets/{dialog-CVMKSdPr.js → dialog-B9j-FMrd.js} +1 -1
  131. package/frontend-dist/assets/{file-text-D0K8Hovo.js → file-text-Bj3ZIo-E.js} +1 -1
  132. package/frontend-dist/assets/index-Bz_ZaXNn.css +1 -0
  133. package/frontend-dist/assets/{index-Ct718O93.js → index-MedWZMHB.js} +1 -1
  134. package/frontend-dist/assets/{lib-H3YI7EK4.js → lib-Hhs3NqfD.js} +1 -1
  135. package/frontend-dist/assets/loader-circle-5TJUukEe.js +1 -0
  136. package/frontend-dist/assets/{useClipboard-Cd7k-5Yq.js → useClipboard-BmmsNSGV.js} +1 -1
  137. package/frontend-dist/assets/{useFocusGuards-luoLXnwV.js → useFocusGuards-A-9V2Y-b.js} +1 -1
  138. package/frontend-dist/assets/useFormControl-DEO19lRe.js +1 -0
  139. package/frontend-dist/assets/{useLogRetention-DB4Iu6o_.js → useLogRetention-BfnBFZ5K.js} +1 -1
  140. package/frontend-dist/assets/useNonce-BfwUJ1Ci.js +1 -0
  141. package/frontend-dist/assets/x-Cfopt3QL.js +1 -0
  142. package/frontend-dist/index.html +20 -20
  143. package/package.json +1 -1
  144. package/frontend-dist/assets/Providers-D8Z97edN.js +0 -1
  145. package/frontend-dist/assets/ProxyEnhancement-Kn8r2SN6.js +0 -5
  146. package/frontend-dist/assets/arrow-down-WyouvE7T.js +0 -1
  147. package/frontend-dist/assets/button-Cnkbp_6J.js +0 -12
  148. package/frontend-dist/assets/check-BuqB5Nyb.js +0 -1
  149. package/frontend-dist/assets/index-xjdbFKXJ.css +0 -1
  150. package/frontend-dist/assets/loader-circle-Be82FnVY.js +0 -1
  151. package/frontend-dist/assets/useFormControl-Da4ViGZF.js +0 -1
  152. package/frontend-dist/assets/useNonce-DvAdQ48J.js +0 -1
  153. package/frontend-dist/assets/x-DB22csQl.js +0 -1
@@ -0,0 +1,130 @@
1
+ import { createRequire } from "module";
2
+ import { existsSync, readdirSync } from "fs";
3
+ import { join, resolve } from "path";
4
+ import { pluginMatches } from "./plugin-types.js";
5
+ import { getAllActiveRules } from "../../db/transform-rules.js";
6
+ const esmRequire = createRequire(import.meta.url);
7
+ export class PluginRegistry {
8
+ plugins = [];
9
+ rulesCache = new Map();
10
+ registerPlugin(plugin) {
11
+ this.plugins.push(plugin);
12
+ }
13
+ loadFromDB(db) {
14
+ const rules = getAllActiveRules(db);
15
+ this.rulesCache.clear();
16
+ for (const rule of rules) {
17
+ this.rulesCache.set(rule.provider_id, rule);
18
+ this.plugins.push(this.ruleToPlugin(rule));
19
+ }
20
+ }
21
+ scanPluginsDir(dir) {
22
+ const resolvedDir = resolve(dir);
23
+ const loaded = [];
24
+ if (!existsSync(resolvedDir)) {
25
+ return loaded;
26
+ }
27
+ const files = readdirSync(resolvedDir).filter((f) => f.endsWith(".js") || f.endsWith(".mjs"));
28
+ for (const file of files) {
29
+ const filePath = join(resolvedDir, file);
30
+ try {
31
+ delete esmRequire.cache[esmRequire.resolve(filePath)];
32
+ const mod = esmRequire(filePath);
33
+ const plugin = mod.default || mod;
34
+ if (!plugin.name) {
35
+ continue;
36
+ }
37
+ this.plugins.push(plugin);
38
+ loaded.push(`${plugin.name} (${file})`);
39
+ // eslint-disable-next-line taste/no-silent-catch -- don't crash server for bad plugin
40
+ }
41
+ catch (err) {
42
+ console.error(`[plugin-registry] Failed to load plugin from ${file}:`, err);
43
+ }
44
+ }
45
+ return loaded;
46
+ }
47
+ getMatchingPlugins(provider) {
48
+ return this.plugins.filter((p) => pluginMatches(p, provider));
49
+ }
50
+ applyBeforeRequest(ctx) {
51
+ for (const p of this.getMatchingPlugins(ctx.provider)) {
52
+ try {
53
+ p.beforeRequestTransform?.(ctx);
54
+ }
55
+ catch (err) {
56
+ console.error(`[plugin-registry] Plugin "${p.name}" beforeRequestTransform error:`, err);
57
+ }
58
+ }
59
+ }
60
+ applyAfterRequest(ctx) {
61
+ for (const p of this.getMatchingPlugins(ctx.provider)) {
62
+ try {
63
+ p.afterRequestTransform?.(ctx);
64
+ }
65
+ catch (err) {
66
+ console.error(`[plugin-registry] Plugin "${p.name}" afterRequestTransform error:`, err);
67
+ }
68
+ }
69
+ }
70
+ applyBeforeResponse(ctx) {
71
+ for (const p of this.getMatchingPlugins(ctx.provider)) {
72
+ try {
73
+ p.beforeResponseTransform?.(ctx);
74
+ }
75
+ catch (err) {
76
+ console.error(`[plugin-registry] Plugin "${p.name}" beforeResponseTransform error:`, err);
77
+ }
78
+ }
79
+ }
80
+ applyAfterResponse(ctx) {
81
+ for (const p of this.getMatchingPlugins(ctx.provider)) {
82
+ try {
83
+ p.afterResponseTransform?.(ctx);
84
+ }
85
+ catch (err) {
86
+ console.error(`[plugin-registry] Plugin "${p.name}" afterResponseTransform error:`, err);
87
+ }
88
+ }
89
+ }
90
+ reload(db, pluginsDir) {
91
+ this.plugins = [];
92
+ this.rulesCache.clear();
93
+ this.loadFromDB(db);
94
+ const loadedPlugins = this.scanPluginsDir(pluginsDir);
95
+ return { loadedPlugins, rulesCount: this.rulesCache.size };
96
+ }
97
+ ruleToPlugin(rule) {
98
+ return {
99
+ name: `declarative:${rule.provider_id}`,
100
+ match: { providerId: rule.provider_id },
101
+ afterRequestTransform(ctx) {
102
+ if (rule.request_defaults) {
103
+ for (const [key, val] of Object.entries(rule.request_defaults)) {
104
+ if (ctx.body[key] === undefined)
105
+ ctx.body[key] = val;
106
+ }
107
+ }
108
+ if (rule.drop_fields) {
109
+ for (const field of rule.drop_fields) {
110
+ delete ctx.body[field];
111
+ }
112
+ }
113
+ if (rule.field_overrides) {
114
+ for (const [key, val] of Object.entries(rule.field_overrides)) {
115
+ ctx.body[key] = val;
116
+ }
117
+ }
118
+ if (rule.inject_headers) {
119
+ for (const [key, val] of Object.entries(rule.inject_headers)) {
120
+ ctx.headers[key] = val;
121
+ }
122
+ }
123
+ },
124
+ afterResponseTransform(_ctx) {
125
+ // field_overrides only applies to request direction;
126
+ // response should reflect actual upstream data, not override rules
127
+ },
128
+ };
129
+ }
130
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Transform Plugin 类型定义
3
+ */
4
+ export interface PluginMatch {
5
+ providerId?: string;
6
+ providerName?: string;
7
+ providerNamePattern?: string;
8
+ apiType?: "openai" | "anthropic";
9
+ }
10
+ export interface RequestTransformContext {
11
+ body: Record<string, unknown>;
12
+ headers: Record<string, string>;
13
+ sourceApiType: "openai" | "anthropic";
14
+ targetApiType: "openai" | "anthropic";
15
+ provider: {
16
+ id: string;
17
+ name: string;
18
+ base_url: string;
19
+ api_type: string;
20
+ };
21
+ }
22
+ export interface ResponseTransformContext {
23
+ response: Record<string, unknown>;
24
+ sourceApiType: "openai" | "anthropic";
25
+ targetApiType: "openai" | "anthropic";
26
+ provider: {
27
+ id: string;
28
+ name: string;
29
+ base_url: string;
30
+ api_type: string;
31
+ };
32
+ }
33
+ export interface TransformPlugin {
34
+ name: string;
35
+ version?: string;
36
+ match: PluginMatch;
37
+ beforeRequestTransform?(ctx: RequestTransformContext): void;
38
+ afterRequestTransform?(ctx: RequestTransformContext): void;
39
+ beforeResponseTransform?(ctx: ResponseTransformContext): void;
40
+ afterResponseTransform?(ctx: ResponseTransformContext): void;
41
+ }
42
+ export declare function pluginMatches(plugin: TransformPlugin, provider: {
43
+ id: string;
44
+ name: string;
45
+ api_type: string;
46
+ }): boolean;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Transform Plugin 类型定义
3
+ */
4
+ export function pluginMatches(plugin, provider) {
5
+ const m = plugin.match;
6
+ if (m.providerId && m.providerId !== provider.id)
7
+ return false;
8
+ if (m.providerName && m.providerName !== provider.name)
9
+ return false;
10
+ if (m.providerNamePattern && !new RegExp(m.providerNamePattern).test(provider.name))
11
+ return false;
12
+ if (m.apiType && m.apiType !== provider.api_type)
13
+ return false;
14
+ return true;
15
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Anthropic Provider-Specific Fields (PSF)
3
+ *
4
+ * Preserves fields that would be lost during OA↔Ant conversion:
5
+ * - thinking signatures (required for multi-turn extended thinking)
6
+ * - citations
7
+ * - redacted_thinking blocks
8
+ * - cache usage metrics
9
+ */
10
+ export interface AnthropicProviderMeta {
11
+ thinking_signatures?: Array<{
12
+ index: number;
13
+ signature: string;
14
+ }>;
15
+ citations?: Array<{
16
+ block_index: number;
17
+ citations: unknown[];
18
+ }>;
19
+ redacted_thinking?: unknown[];
20
+ cache_usage?: {
21
+ cache_read_input_tokens?: number;
22
+ cache_creation_input_tokens?: number;
23
+ };
24
+ }
25
+ export declare function extractAnthropicMeta(antResponse: Record<string, unknown>): AnthropicProviderMeta | undefined;
26
+ export declare function stripProviderMeta(body: Record<string, unknown>): {
27
+ meta: AnthropicProviderMeta | undefined;
28
+ body: Record<string, unknown>;
29
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Anthropic Provider-Specific Fields (PSF)
3
+ *
4
+ * Preserves fields that would be lost during OA↔Ant conversion:
5
+ * - thinking signatures (required for multi-turn extended thinking)
6
+ * - citations
7
+ * - redacted_thinking blocks
8
+ * - cache usage metrics
9
+ */
10
+ export function extractAnthropicMeta(antResponse) {
11
+ const content = antResponse.content;
12
+ if (!content)
13
+ return undefined;
14
+ const meta = {};
15
+ let hasMeta = false;
16
+ // thinking signatures — needed for multi-turn extended thinking
17
+ const signatures = [];
18
+ for (let i = 0; i < content.length; i++) {
19
+ if (content[i].type === "thinking" && content[i].signature) {
20
+ signatures.push({
21
+ index: i,
22
+ signature: content[i].signature,
23
+ });
24
+ }
25
+ }
26
+ if (signatures.length > 0) {
27
+ meta.thinking_signatures = signatures;
28
+ hasMeta = true;
29
+ }
30
+ // redacted_thinking blocks
31
+ const redacted = content.filter((b) => b.type === "redacted_thinking");
32
+ if (redacted.length > 0) {
33
+ meta.redacted_thinking = redacted;
34
+ hasMeta = true;
35
+ }
36
+ // citations
37
+ const citations = [];
38
+ for (let i = 0; i < content.length; i++) {
39
+ if (content[i].citations) {
40
+ citations.push({
41
+ block_index: i,
42
+ citations: content[i].citations,
43
+ });
44
+ }
45
+ }
46
+ if (citations.length > 0) {
47
+ meta.citations = citations;
48
+ hasMeta = true;
49
+ }
50
+ // cache usage from top-level usage
51
+ const usage = antResponse.usage;
52
+ if (usage?.cache_read_input_tokens != null ||
53
+ usage?.cache_creation_input_tokens != null) {
54
+ meta.cache_usage = {
55
+ cache_read_input_tokens: usage.cache_read_input_tokens,
56
+ cache_creation_input_tokens: usage.cache_creation_input_tokens,
57
+ };
58
+ hasMeta = true;
59
+ }
60
+ return hasMeta ? meta : undefined;
61
+ }
62
+ export function stripProviderMeta(body) {
63
+ const pm = body.provider_meta;
64
+ if (!pm)
65
+ return { meta: undefined, body };
66
+ const cleaned = { ...body };
67
+ delete cleaned.provider_meta;
68
+ return {
69
+ meta: pm.anthropic,
70
+ body: cleaned,
71
+ };
72
+ }
@@ -0,0 +1,4 @@
1
+ export declare function openaiToAnthropicRequest(body: Record<string, unknown>): Record<string, unknown>;
2
+ export declare function anthropicToOpenAIRequest(body: Record<string, unknown>): Record<string, unknown>;
3
+ /** Entry point: transform request body based on direction */
4
+ export declare function transformRequestBody(body: Record<string, unknown>, sourceApiType: string, targetApiType: string, _model: string): Record<string, unknown>;
@@ -0,0 +1,151 @@
1
+ import { convertMessagesOA2Ant, convertMessagesAnt2OA } from "./message-mapper.js";
2
+ import { convertToolsOA2Ant, convertToolsAnt2OA, mapToolChoiceOA2Ant, mapToolChoiceAnt2OA } from "./tool-mapper.js";
3
+ import { mapReasoningToThinking, mapThinkingToReasoning } from "./thinking-mapper.js";
4
+ import { stripProviderMeta } from "./provider-meta.js";
5
+ const DEFAULT_MAX_TOKENS = 4096;
6
+ const OA_KNOWN_FIELDS = new Set([
7
+ "model", "messages", "max_completion_tokens", "max_tokens",
8
+ "stop", "temperature", "top_p", "stream", "tools", "tool_choice",
9
+ "parallel_tool_calls", "reasoning", "user", "n", "stream_options",
10
+ "response_format", "provider_meta",
11
+ ]);
12
+ const ANT_KNOWN_FIELDS = new Set([
13
+ "model", "system", "messages", "max_tokens",
14
+ "stop_sequences", "temperature", "top_p", "stream", "tools", "tool_choice",
15
+ "thinking", "metadata",
16
+ ]);
17
+ /** Log dropped fields for debugging */
18
+ function logDroppedFields(body, known, direction) {
19
+ const dropped = Object.keys(body).filter(k => !known.has(k));
20
+ if (dropped.length > 0) {
21
+ console.warn(`[request-transform] ${direction}: dropped unknown fields: ${dropped.join(", ")}`);
22
+ }
23
+ }
24
+ export function openaiToAnthropicRequest(body) {
25
+ // strip provider_meta before processing, restore PSF to messages later
26
+ const { meta: antMeta, body: cleanedBody } = stripProviderMeta(body);
27
+ const result = {};
28
+ result.model = cleanedBody.model;
29
+ const { system, messages } = convertMessagesOA2Ant(cleanedBody.messages ?? []);
30
+ if (system != null)
31
+ result.system = system;
32
+ result.messages = messages;
33
+ result.max_tokens = cleanedBody.max_completion_tokens ?? cleanedBody.max_tokens ?? DEFAULT_MAX_TOKENS;
34
+ if (cleanedBody.stop != null) {
35
+ result.stop_sequences = Array.isArray(cleanedBody.stop) ? cleanedBody.stop : [cleanedBody.stop];
36
+ }
37
+ if (cleanedBody.temperature != null)
38
+ result.temperature = cleanedBody.temperature;
39
+ if (cleanedBody.top_p != null)
40
+ result.top_p = cleanedBody.top_p;
41
+ if (cleanedBody.stream != null)
42
+ result.stream = cleanedBody.stream;
43
+ if (cleanedBody.tool_choice === "none" || (typeof cleanedBody.tool_choice === "object" && cleanedBody.tool_choice.type === "none")) {
44
+ // Anthropic has no "none" tool_choice — skip tools entirely
45
+ }
46
+ else {
47
+ if (cleanedBody.tools) {
48
+ result.tools = convertToolsOA2Ant(cleanedBody.tools);
49
+ }
50
+ if (cleanedBody.tool_choice != null) {
51
+ const mapped = mapToolChoiceOA2Ant(cleanedBody.tool_choice);
52
+ if (mapped != null) {
53
+ result.tool_choice = cleanedBody.parallel_tool_calls === false
54
+ ? { ...mapped, disable_parallel_tool_use: true }
55
+ : mapped;
56
+ }
57
+ }
58
+ else if (cleanedBody.parallel_tool_calls === false) {
59
+ result.tool_choice = { type: "auto", disable_parallel_tool_use: true };
60
+ }
61
+ }
62
+ if (cleanedBody.reasoning) {
63
+ const thinking = mapReasoningToThinking(cleanedBody.reasoning);
64
+ result.thinking = thinking;
65
+ if (thinking.budget_tokens && result.max_tokens < thinking.budget_tokens) {
66
+ result.max_tokens = thinking.budget_tokens;
67
+ }
68
+ }
69
+ if (cleanedBody.user) {
70
+ result.metadata = { user_id: cleanedBody.user };
71
+ }
72
+ if (cleanedBody.response_format) {
73
+ console.warn("[request-transform] response_format dropped: Anthropic has no JSON mode");
74
+ }
75
+ // PSF restore in single pass: signatures + redacted blocks
76
+ if (antMeta?.thinking_signatures?.length || antMeta?.redacted_thinking?.length) {
77
+ let sigIdx = 0;
78
+ let redactedApplied = false;
79
+ for (const msg of result.messages) {
80
+ if (msg.role !== "assistant")
81
+ continue;
82
+ const content = msg.content;
83
+ if (!content)
84
+ continue;
85
+ if (antMeta?.redacted_thinking?.length && !redactedApplied) {
86
+ msg.content = [...antMeta.redacted_thinking, ...content];
87
+ redactedApplied = true;
88
+ }
89
+ if (antMeta?.thinking_signatures?.length) {
90
+ for (const block of msg.content) {
91
+ if (block.type === "thinking" && sigIdx < antMeta.thinking_signatures.length) {
92
+ block.signature = antMeta.thinking_signatures[sigIdx].signature;
93
+ sigIdx++;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ logDroppedFields(cleanedBody, OA_KNOWN_FIELDS, "OA→Ant");
100
+ return result;
101
+ }
102
+ export function anthropicToOpenAIRequest(body) {
103
+ const result = {};
104
+ result.model = body.model;
105
+ const antMessages = body.messages ?? [];
106
+ result.messages = convertMessagesAnt2OA(body.system, antMessages);
107
+ if (body.max_tokens != null)
108
+ result.max_completion_tokens = body.max_tokens;
109
+ if (body.stop_sequences)
110
+ result.stop = body.stop_sequences;
111
+ if (body.temperature != null)
112
+ result.temperature = body.temperature;
113
+ if (body.top_p != null)
114
+ result.top_p = body.top_p;
115
+ if (body.stream != null)
116
+ result.stream = body.stream;
117
+ if (body.stream === true) {
118
+ result.stream_options = { include_usage: true };
119
+ }
120
+ if (body.tools) {
121
+ result.tools = convertToolsAnt2OA(body.tools);
122
+ }
123
+ if (body.tool_choice != null) {
124
+ result.tool_choice = mapToolChoiceAnt2OA(body.tool_choice);
125
+ }
126
+ if (body.thinking) {
127
+ const reasoning = mapThinkingToReasoning(body.thinking);
128
+ if (reasoning)
129
+ result.reasoning = reasoning;
130
+ }
131
+ const metadata = body.metadata;
132
+ if (metadata?.user_id) {
133
+ result.user = metadata.user_id;
134
+ }
135
+ logDroppedFields(body, ANT_KNOWN_FIELDS, "Ant→OA");
136
+ return result;
137
+ }
138
+ /** Entry point: transform request body based on direction */
139
+ export function transformRequestBody(body, sourceApiType, targetApiType,
140
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
141
+ _model) {
142
+ if (sourceApiType === targetApiType)
143
+ return body;
144
+ if (sourceApiType === "openai" && targetApiType === "anthropic") {
145
+ return openaiToAnthropicRequest(body);
146
+ }
147
+ if (sourceApiType === "anthropic" && targetApiType === "openai") {
148
+ return anthropicToOpenAIRequest(body);
149
+ }
150
+ return body;
151
+ }
@@ -0,0 +1,4 @@
1
+ export declare function openaiResponseToAnthropic(bodyStr: string): string;
2
+ export declare function anthropicResponseToOpenAI(bodyStr: string): string;
3
+ export declare function transformResponseBody(bodyStr: string, sourceApiType: string, targetApiType: string): string;
4
+ export declare function transformErrorResponse(bodyStr: string, sourceApiType: string, targetApiType: string): string;
@@ -0,0 +1,99 @@
1
+ import { generateMsgId, generateChatcmplId, MS_PER_SECOND } from "./id-utils.js";
2
+ import { mapFinishReasonToStopReason, mapStopReasonToFinishReason, mapUsageOA2Ant, mapUsageAnt2OA } from "./usage-mapper.js";
3
+ import { extractAnthropicMeta } from "./provider-meta.js";
4
+ import { parseToolArguments } from "./sanitize.js";
5
+ export function openaiResponseToAnthropic(bodyStr) {
6
+ const oai = JSON.parse(bodyStr);
7
+ const choice = oai.choices?.[0];
8
+ const msg = choice?.message;
9
+ const content = [];
10
+ // reasoning_content → thinking block (first)
11
+ if (msg?.reasoning_content) {
12
+ content.push({ type: "thinking", thinking: msg.reasoning_content });
13
+ }
14
+ // text content
15
+ if (msg?.content) {
16
+ content.push({ type: "text", text: msg.content });
17
+ }
18
+ // tool_calls → tool_use blocks
19
+ if (msg?.tool_calls) {
20
+ for (const tc of msg.tool_calls) {
21
+ const input = parseToolArguments(tc.function.arguments);
22
+ content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input });
23
+ }
24
+ }
25
+ if (content.length === 0)
26
+ content.push({ type: "text", text: "" });
27
+ return JSON.stringify({
28
+ id: generateMsgId(),
29
+ type: "message",
30
+ role: "assistant",
31
+ content,
32
+ model: oai.model,
33
+ stop_reason: mapFinishReasonToStopReason(choice?.finish_reason ?? "stop"),
34
+ stop_sequence: null,
35
+ usage: mapUsageOA2Ant(oai.usage),
36
+ });
37
+ }
38
+ export function anthropicResponseToOpenAI(bodyStr) {
39
+ const ant = JSON.parse(bodyStr);
40
+ const blocks = (ant.content ?? []);
41
+ const thinkingText = blocks.filter(b => b.type === "thinking").map(b => b.thinking).join("");
42
+ const textContent = blocks.filter(b => b.type === "text").map(b => b.text).join("");
43
+ const toolBlocks = blocks.filter(b => b.type === "tool_use");
44
+ const message = { role: "assistant" };
45
+ if (thinkingText)
46
+ message.reasoning_content = thinkingText;
47
+ if (textContent)
48
+ message.content = textContent;
49
+ if (toolBlocks.length > 0) {
50
+ message.tool_calls = toolBlocks.map(b => ({
51
+ id: b.id,
52
+ type: "function",
53
+ function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) },
54
+ }));
55
+ }
56
+ // preserve Anthropic-specific fields that would be lost in conversion
57
+ const antMeta = extractAnthropicMeta(ant);
58
+ const result = {
59
+ id: ant.id ?? generateChatcmplId(),
60
+ object: "chat.completion",
61
+ created: Math.floor(Date.now() / MS_PER_SECOND),
62
+ model: ant.model,
63
+ choices: [{ index: 0, message, finish_reason: mapStopReasonToFinishReason(ant.stop_reason ?? "end_turn") }],
64
+ usage: mapUsageAnt2OA(ant.usage),
65
+ };
66
+ if (antMeta) {
67
+ result.provider_meta = { anthropic: antMeta };
68
+ }
69
+ return JSON.stringify(result);
70
+ }
71
+ export function transformResponseBody(bodyStr, sourceApiType, targetApiType) {
72
+ if (sourceApiType === targetApiType)
73
+ return bodyStr;
74
+ if (sourceApiType === "openai" && targetApiType === "anthropic")
75
+ return openaiResponseToAnthropic(bodyStr);
76
+ if (sourceApiType === "anthropic" && targetApiType === "openai")
77
+ return anthropicResponseToOpenAI(bodyStr);
78
+ return bodyStr;
79
+ }
80
+ export function transformErrorResponse(bodyStr, sourceApiType, targetApiType) {
81
+ if (sourceApiType === targetApiType)
82
+ return bodyStr;
83
+ try {
84
+ if (sourceApiType === "anthropic" && targetApiType === "openai") {
85
+ const ant = JSON.parse(bodyStr);
86
+ const err = ant.error ?? {};
87
+ return JSON.stringify({ error: { message: err.message ?? "Unknown error", type: err.type ?? "api_error", code: "upstream_error" } });
88
+ }
89
+ if (sourceApiType === "openai" && targetApiType === "anthropic") {
90
+ const oai = JSON.parse(bodyStr);
91
+ const err = oai.error ?? {};
92
+ return JSON.stringify({ type: "error", error: { type: err.type ?? "api_error", message: err.message ?? "Unknown error" } });
93
+ }
94
+ }
95
+ catch {
96
+ return bodyStr;
97
+ }
98
+ return bodyStr;
99
+ }
@@ -0,0 +1,3 @@
1
+ export declare function sanitizeToolUseId(id: string): string;
2
+ export declare function parseToolArguments(args: unknown): Record<string, unknown>;
3
+ export declare function ensureNonEmptyContent(messages: unknown[]): void;
@@ -0,0 +1,24 @@
1
+ export function sanitizeToolUseId(id) {
2
+ const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_");
3
+ return sanitized || "toolu_unknown";
4
+ }
5
+ export function parseToolArguments(args) {
6
+ try {
7
+ return JSON.parse(String(args ?? "{}"));
8
+ }
9
+ catch {
10
+ console.warn("[transform] Failed to parse tool arguments, using empty object");
11
+ return {};
12
+ }
13
+ }
14
+ export function ensureNonEmptyContent(messages) {
15
+ for (const msg of messages) {
16
+ const m = msg;
17
+ if (m.role === "assistant")
18
+ continue;
19
+ if (!m.content || m.content === "" ||
20
+ (Array.isArray(m.content) && m.content.length === 0)) {
21
+ m.content = " ";
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,20 @@
1
+ import { BaseSSETransform } from "./stream-transform-base.js";
2
+ export declare class AnthropicToOpenAITransform extends BaseSSETransform {
3
+ private chatcmplId;
4
+ private firstContentBlock;
5
+ private inputTokens;
6
+ private outputTokens;
7
+ private finishReasonEmitted;
8
+ private currentToolCallIndex;
9
+ private blockToToolCallIndex;
10
+ private contentBlockTypes;
11
+ private contentBlockSignatures;
12
+ private thinkingSignatures;
13
+ private cacheUsage;
14
+ protected processEvent(event: {
15
+ event?: string;
16
+ data?: string;
17
+ }): void;
18
+ protected flushPendingData(): void;
19
+ protected ensureTerminated(): void;
20
+ }