kimi-proxy 0.1.1 → 0.1.3

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.
@@ -16,7 +16,7 @@ export interface JsonObject {
16
16
  [key: string]: JsonValue | undefined;
17
17
  }
18
18
  export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
19
- export declare function isJsonObject(value: JsonValue): value is JsonObject;
19
+ export declare function isJsonObject(value: JsonValue | undefined): value is JsonObject;
20
20
  export declare enum Operation {
21
21
  Chat = "chat",
22
22
  Messages = "messages",
@@ -2792,3 +2792,5 @@ export interface PipelineResult {
2792
2792
  }
2793
2793
  export type Json = JsonValue;
2794
2794
  export declare const isMessage: (value: unknown) => value is Message;
2795
+ export declare function getToolSchemas(request: Request): Record<string, z.ZodSchema>;
2796
+ export declare function findMatchingTool(request: Request, args: unknown): string | null;
package/dist/index.js CHANGED
@@ -1018,6 +1018,77 @@ var ResponseSchema = z3.object({
1018
1018
  synthetic: z3.boolean().optional()
1019
1019
  }).optional()
1020
1020
  });
1021
+ function jsonSchemaToZod(schema) {
1022
+ if (!schema || typeof schema !== "object" || Array.isArray(schema))
1023
+ return z3.any();
1024
+ const s = schema;
1025
+ switch (s.type) {
1026
+ case "object": {
1027
+ const shape = {};
1028
+ const properties = s.properties || {};
1029
+ const required = s.required || [];
1030
+ for (const key in properties) {
1031
+ let propSchema = jsonSchemaToZod(properties[key]);
1032
+ if (!required.includes(key)) {
1033
+ propSchema = propSchema.optional();
1034
+ }
1035
+ shape[key] = propSchema;
1036
+ }
1037
+ const zodObj = z3.object(shape);
1038
+ if (s.additionalProperties === false) {
1039
+ return zodObj.strict();
1040
+ } else {
1041
+ return zodObj.passthrough();
1042
+ }
1043
+ }
1044
+ case "array":
1045
+ return z3.array(jsonSchemaToZod(s.items || {}));
1046
+ case "string":
1047
+ if (Array.isArray(s.enum) && s.enum.length > 0 && s.enum.every((e) => typeof e === "string")) {
1048
+ return z3.enum(s.enum);
1049
+ }
1050
+ return z3.string();
1051
+ case "number":
1052
+ case "integer":
1053
+ return z3.number();
1054
+ case "boolean":
1055
+ return z3.boolean();
1056
+ case "null":
1057
+ return z3.null();
1058
+ default:
1059
+ return z3.any();
1060
+ }
1061
+ }
1062
+ var toolSchemaCache = new WeakMap;
1063
+ function getToolSchemas(request) {
1064
+ let schemas = toolSchemaCache.get(request);
1065
+ if (schemas)
1066
+ return schemas;
1067
+ schemas = {};
1068
+ for (const tool of request.tools ?? []) {
1069
+ schemas[tool.name] = jsonSchemaToZod(tool.parameters);
1070
+ }
1071
+ toolSchemaCache.set(request, schemas);
1072
+ return schemas;
1073
+ }
1074
+ function safeParseJson(str) {
1075
+ try {
1076
+ return JSON.parse(str);
1077
+ } catch {
1078
+ return str;
1079
+ }
1080
+ }
1081
+ function findMatchingTool(request, args) {
1082
+ const schemas = getToolSchemas(request);
1083
+ const matches = [];
1084
+ const parsedArgs = typeof args === "string" ? safeParseJson(args) : args;
1085
+ for (const [name, schema] of Object.entries(schemas)) {
1086
+ if (schema.safeParse(parsedArgs).success) {
1087
+ matches.push(name);
1088
+ }
1089
+ }
1090
+ return matches.length === 1 ? matches[0] : null;
1091
+ }
1021
1092
 
1022
1093
  // src/core/ensureToolCall.ts
1023
1094
  var ENSURE_TOOL_CALL_STATE_KEY = "__ensureToolCall";
@@ -2635,13 +2706,39 @@ function parseToolCalls(section) {
2635
2706
  }
2636
2707
  return toolCalls;
2637
2708
  }
2638
- function fixKimiResponse(response) {
2709
+ function repairToolNames(toolCalls, request) {
2710
+ const schemas = getToolSchemas(request);
2711
+ const toolNames = new Set(Object.keys(schemas));
2712
+ let repairedCount = 0;
2713
+ for (const call of toolCalls) {
2714
+ if (call.type !== "function" || !isJsonObject(call.function))
2715
+ continue;
2716
+ const fn = call.function;
2717
+ if (typeof fn.name === "number") {
2718
+ fn.name = String(fn.name);
2719
+ }
2720
+ const name = fn.name;
2721
+ if (typeof name !== "string")
2722
+ continue;
2723
+ if (toolNames.has(name))
2724
+ continue;
2725
+ const match = findMatchingTool(request, fn.arguments);
2726
+ if (match) {
2727
+ logger.debug(`[KimiFixer] Repaired tool name: ${name} -> ${match}`);
2728
+ fn.name = match;
2729
+ repairedCount++;
2730
+ }
2731
+ }
2732
+ return repairedCount;
2733
+ }
2734
+ function fixKimiResponse(response, request) {
2639
2735
  const metadata = {
2640
2736
  extractedToolCalls: 0,
2641
2737
  extractedFromReasoning: 0,
2642
2738
  extractedFromContent: 0,
2643
2739
  cleanedReasoningContent: false,
2644
- cleanedMessageContent: false
2740
+ cleanedMessageContent: false,
2741
+ repairedToolNames: 0
2645
2742
  };
2646
2743
  try {
2647
2744
  const choices = response?.choices;
@@ -2698,6 +2795,10 @@ ${thinkingContent}` : thinkingContent;
2698
2795
  }
2699
2796
  }
2700
2797
  if (aggregatedToolCalls.length) {
2798
+ const repaired = repairToolNames(aggregatedToolCalls, request);
2799
+ if (repaired > 0) {
2800
+ metadata.repairedToolNames = (metadata.repairedToolNames || 0) + repaired;
2801
+ }
2701
2802
  message.tool_calls = aggregatedToolCalls;
2702
2803
  choice.finish_reason = "tool_calls";
2703
2804
  } else if ("tool_calls" in message) {
@@ -2741,7 +2842,7 @@ var OpenAIToolCallSchema = z7.object({
2741
2842
  id: z7.string().optional(),
2742
2843
  type: z7.literal("function").optional(),
2743
2844
  function: z7.object({
2744
- name: z7.string(),
2845
+ name: z7.union([z7.string(), z7.number()]).transform(String),
2745
2846
  arguments: z7.union([z7.string(), z7.record(z7.unknown())]).optional()
2746
2847
  }).passthrough()
2747
2848
  }).passthrough();
@@ -2771,13 +2872,14 @@ function normalizeToolCalls(toolCalls) {
2771
2872
  const normalized = [];
2772
2873
  for (const call of toolCalls) {
2773
2874
  const fn = call?.function;
2774
- if (!fn || typeof fn.name !== "string")
2875
+ if (!fn || typeof fn.name !== "string" && typeof fn.name !== "number")
2775
2876
  continue;
2776
- const id = typeof call.id === "string" && call.id.length ? call.id : fn.name;
2877
+ const name = String(fn.name);
2878
+ const id = typeof call.id === "string" && call.id.length ? call.id : `${name}_call_${Math.random().toString(36).substring(2, 10)}`;
2777
2879
  normalized.push({
2778
2880
  id,
2779
2881
  type: "function",
2780
- name: fn.name,
2882
+ name,
2781
2883
  arguments: safeJsonString(fn.arguments)
2782
2884
  });
2783
2885
  }
@@ -2879,7 +2981,7 @@ function toOpenAIMessages(messages) {
2879
2981
  };
2880
2982
  });
2881
2983
  }
2882
- function normalizeOpenAIProviderResponse(payload) {
2984
+ function normalizeOpenAIProviderResponse(payload, request) {
2883
2985
  const parsed = OpenAIResponseSchema.safeParse(payload.body);
2884
2986
  if (!parsed.success) {
2885
2987
  const issue = parsed.error.issues[0]?.message ?? "Invalid provider payload";
@@ -2887,7 +2989,7 @@ function normalizeOpenAIProviderResponse(payload) {
2887
2989
  return { error: issue };
2888
2990
  }
2889
2991
  const cloned = structuredClone(parsed.data);
2890
- const { response, metadata } = fixKimiResponse(cloned);
2992
+ const { response, metadata } = fixKimiResponse(cloned, request);
2891
2993
  const normalized = OpenAIResponseSchema.safeParse(response);
2892
2994
  if (!normalized.success) {
2893
2995
  const issue = normalized.error.issues[0]?.message ?? "Provider payload invalid after normalization";
@@ -3002,7 +3104,7 @@ class OpenAIProviderAdapter {
3002
3104
  if (payload.status >= 400) {
3003
3105
  return toUlxErrorResponse(payload, request);
3004
3106
  }
3005
- const normalized = normalizeOpenAIProviderResponse(payload);
3107
+ const normalized = normalizeOpenAIProviderResponse(payload, request);
3006
3108
  if ("error" in normalized) {
3007
3109
  return {
3008
3110
  id: request.id,
@@ -3090,7 +3192,7 @@ class OpenRouterProviderAdapter {
3090
3192
  if (payload.status >= 400) {
3091
3193
  return toUlxErrorResponse(payload, request);
3092
3194
  }
3093
- const normalized = normalizeOpenAIProviderResponse(payload);
3195
+ const normalized = normalizeOpenAIProviderResponse(payload, request);
3094
3196
  if ("error" in normalized) {
3095
3197
  return {
3096
3198
  id: request.id,
@@ -3422,7 +3524,7 @@ class VertexProviderAdapter {
3422
3524
  return toUlxErrorResponse(payload, request);
3423
3525
  }
3424
3526
  if (request.model && MAAS_MODEL_PATTERN.test(request.model)) {
3425
- const normalized = normalizeOpenAIProviderResponse(payload);
3527
+ const normalized = normalizeOpenAIProviderResponse(payload, request);
3426
3528
  if ("error" in normalized) {
3427
3529
  return {
3428
3530
  id: request.id,
@@ -3977,7 +4079,7 @@ ${content}`
3977
4079
  logger.warn({ requestId: request.id }, "Detected prior assistant termination; requesting synthetic response");
3978
4080
  return true;
3979
4081
  }
3980
- if (toolCalls.length === 1 && this.checkTerminationHeuristic(message, toolCalls[0])) {
4082
+ if (toolCalls.length === 1 && this.checkTerminationHeuristic(message, toolCalls[0], request)) {
3981
4083
  requestSyntheticResponse(request.state);
3982
4084
  logger.warn({ requestId: request.id }, "Detected TodoWrite termination heuristic; requesting synthetic response");
3983
4085
  return true;
@@ -3985,7 +4087,10 @@ ${content}`
3985
4087
  }
3986
4088
  return false;
3987
4089
  }
3988
- checkTerminationHeuristic(message, toolCall) {
4090
+ checkTerminationHeuristic(message, toolCall, request) {
4091
+ if (!request.model.toLowerCase().includes("kimi")) {
4092
+ return false;
4093
+ }
3989
4094
  if (!hasCaseInsensitiveTerminationKeywords(message.content)) {
3990
4095
  return false;
3991
4096
  }
@@ -4014,7 +4119,7 @@ class EnsureToolCallResponseTransform {
4014
4119
  const response = context.response;
4015
4120
  if (!ensureState || !response)
4016
4121
  return;
4017
- if (this.checkTerminationHeuristic(response)) {
4122
+ if (this.checkTerminationHeuristic(response, context.request)) {
4018
4123
  ensureState.pendingReminder = false;
4019
4124
  logger.info({ requestId: context.request.id }, "EnsureToolCall satisfied by TodoWrite + keyword heuristics");
4020
4125
  return;
@@ -4058,6 +4163,8 @@ class EnsureToolCallResponseTransform {
4058
4163
  }
4059
4164
  if (!hasContent && finalAnswer) {
4060
4165
  messageBlock.content = [{ type: "text", text: finalAnswer }];
4166
+ } else if (hasContent && finalAnswer) {
4167
+ messageBlock.content.push({ type: "text", text: finalAnswer });
4061
4168
  }
4062
4169
  continue;
4063
4170
  }
@@ -4086,7 +4193,10 @@ class EnsureToolCallResponseTransform {
4086
4193
  }
4087
4194
  return false;
4088
4195
  }
4089
- checkTerminationHeuristic(response) {
4196
+ checkTerminationHeuristic(response, request) {
4197
+ if (!request.model.toLowerCase().includes("kimi")) {
4198
+ return false;
4199
+ }
4090
4200
  const messageBlock = response.output.find((block) => block.type === "message");
4091
4201
  if (!messageBlock || messageBlock.type !== "message")
4092
4202
  return false;
@@ -4189,7 +4299,7 @@ class ModelRegistry {
4189
4299
  }));
4190
4300
  }
4191
4301
  resolve(modelName, profile) {
4192
- let variants = this.groups.get(modelName);
4302
+ let variants = this.groups.get(modelName)?.filter((v) => !v.profile || v.profile === profile);
4193
4303
  if (variants && variants.length > 0 && profile) {
4194
4304
  const filtered = variants.filter((v) => v.profile === profile);
4195
4305
  if (filtered.length > 0) {
@@ -5697,13 +5807,15 @@ async function handleRequest(req, reply, body, modelRegistry, pipeline, logStore
5697
5807
  method: req.method,
5698
5808
  url: req.url,
5699
5809
  statusCode: 400,
5810
+ provider: "schema_validation_failed",
5700
5811
  startedAt,
5701
5812
  finishedAt: Date.now(),
5702
5813
  requestBody: req.body,
5703
5814
  responseBody: errorBody,
5704
5815
  operation: options.operation,
5705
5816
  clientFormat: options.clientFormat,
5706
- profile: options.profile
5817
+ profile: options.profile,
5818
+ summary: summarizeError(error, "schema_validation")
5707
5819
  });
5708
5820
  reply.status(400).send(errorBody);
5709
5821
  return;
@@ -5728,13 +5840,15 @@ async function handleRequest(req, reply, body, modelRegistry, pipeline, logStore
5728
5840
  url: req.url,
5729
5841
  statusCode: 400,
5730
5842
  model,
5843
+ provider: "resolution_failed",
5731
5844
  startedAt,
5732
5845
  finishedAt: Date.now(),
5733
5846
  requestBody: parsedBody,
5734
5847
  responseBody: errorBody,
5735
5848
  operation: options.operation,
5736
5849
  clientFormat: options.clientFormat,
5737
- profile: options.profile
5850
+ profile: options.profile,
5851
+ summary: summarizeError(error, "model_resolution")
5738
5852
  });
5739
5853
  reply.status(400).send(errorBody);
5740
5854
  return;
@@ -5811,7 +5925,8 @@ async function handleRequest(req, reply, body, modelRegistry, pipeline, logStore
5811
5925
  requestBody: upstreamModel ? { ...parsedBody, model: upstreamModel } : parsedBody,
5812
5926
  responseBody: { error: errorDetails },
5813
5927
  providerRequestBody: null,
5814
- providerResponseBody: null
5928
+ providerResponseBody: null,
5929
+ summary: summarizeError(error, "pipeline_execution")
5815
5930
  });
5816
5931
  reply.status(500).send({ error: { message: "Internal proxy error" } });
5817
5932
  }
@@ -5900,6 +6015,13 @@ function summarize(response) {
5900
6015
  preview
5901
6016
  });
5902
6017
  }
6018
+ function summarizeError(error, errorType) {
6019
+ const errorMessage = error instanceof Error ? error.message : String(error);
6020
+ return JSON.stringify({
6021
+ error_type: errorType,
6022
+ error_message: errorMessage.slice(0, 500)
6023
+ });
6024
+ }
5903
6025
  function isPrematureCloseError(error) {
5904
6026
  if (!error || typeof error !== "object")
5905
6027
  return false;
@@ -5923,5 +6045,5 @@ async function bootstrap() {
5923
6045
  }
5924
6046
  bootstrap();
5925
6047
 
5926
- //# debugId=78CDAC4F2B51930964756E2164756E21
6048
+ //# debugId=49E2D016D6E3465664756E2164756E21
5927
6049
  //# sourceMappingURL=index.js.map