Skip to content

Commit 1e22464

Browse files
committed
refactor(openai-chat): extract repeated reasoning/stop-reason/choices helpers
Behavior-preserving simplification of the ClinePass adapter changes: - stopReasonFor() single-sources the finish_reason mapping used by the stream, EOF-residual, and non-stream done events. - reasoningTextFrom() shares the reasoning_content/reasoning delta extraction between stream and non-stream paths. - invalidChoicesEvent() replaces the three identical invalid-choices error constructions. - closeToolCalls() unifies the flush/discard/finally call-closing loops, and terminateWithError() shares the terminal stream error sequence.
1 parent ec81f7f commit 1e22464

1 file changed

Lines changed: 48 additions & 53 deletions

File tree

src/adapters/openai-chat.ts

Lines changed: 48 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,30 @@ function upstreamErrorEvent(
143143
};
144144
}
145145

146+
function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
147+
return finishReason === "length"
148+
? "max_tokens"
149+
: finishReason === "content_filter"
150+
? "content_filter"
151+
: undefined;
152+
}
153+
154+
function reasoningTextFrom(record: Record<string, unknown>): string | undefined {
155+
return typeof record.reasoning_content === "string" && record.reasoning_content.length > 0
156+
? record.reasoning_content
157+
: typeof record.reasoning === "string" && record.reasoning.length > 0
158+
? record.reasoning
159+
: undefined;
160+
}
161+
162+
function invalidChoicesEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
163+
return {
164+
type: "error",
165+
message: "upstream response contained invalid choices",
166+
...(usage !== undefined ? { usage } : {}),
167+
};
168+
}
169+
146170
function developerSystemText(message: OcxMessage): string | undefined {
147171
if (message.role !== "developer") return undefined;
148172
if (typeof message.content === "string") return message.content;
@@ -797,21 +821,28 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
797821
interface PendingToolCall { key: string; id: string; name: string; args: string; argsBytes: number }
798822
const pendingToolCalls: PendingToolCall[] = [];
799823
let toolCallSeq = 0;
824+
const closeToolCalls = (): PendingToolCall[] => {
825+
const calls = [...pendingToolCalls];
826+
for (const call of calls) budget.closeCall(call.key);
827+
pendingToolCalls.length = 0;
828+
return calls;
829+
};
800830
const flushToolCalls = function* (): Generator<AdapterEvent> {
801831
// Do not treat flushed tool calls as user-facing output for the finish-less EOF
802832
// fallback — incomplete tool args must stay on the truncation path.
803-
for (const call of pendingToolCalls) {
833+
for (const call of closeToolCalls()) {
804834
if (!call.id) call.id = `call_${++toolCallSeq}`;
805835
yield { type: "tool_call_start", id: call.id, name: call.name };
806836
if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
807837
yield { type: "tool_call_end" };
808-
budget.closeCall(call.key);
809838
}
810-
pendingToolCalls.length = 0;
811839
};
812-
const discardToolCalls = (): void => {
813-
for (const call of pendingToolCalls) budget.closeCall(call.key);
814-
pendingToolCalls.length = 0;
840+
const terminateWithError = function* (
841+
event: Extract<AdapterEvent, { type: "error" }>,
842+
): Generator<AdapterEvent, "terminate"> {
843+
closeToolCalls();
844+
yield event;
845+
return "terminate";
815846
};
816847
let pendingUsage: OcxUsage | undefined;
817848
// Track terminal signals so a socket EOF without any terminator can fail closed instead of
@@ -832,11 +863,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
832863
const payload = line.slice(6).trim();
833864
if (payload === "[DONE]") {
834865
yield* flushToolCalls();
835-
const stopReason = finishReason === "length"
836-
? "max_tokens"
837-
: finishReason === "content_filter"
838-
? "content_filter"
839-
: undefined;
866+
const stopReason = stopReasonFor(finishReason);
840867
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
841868
return "terminate";
842869
}
@@ -855,9 +882,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
855882
if (chunk.error !== undefined && chunk.error !== null) {
856883
const event = upstreamErrorEvent(chunk.error, pendingUsage);
857884
debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
858-
discardToolCalls();
859-
yield event;
860-
return "terminate";
885+
return yield* terminateWithError(event);
861886
}
862887

863888
if (chunk.usage) {
@@ -870,24 +895,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
870895
const choices = chunk.choices;
871896
if (choices === undefined) return "continue";
872897
if (!Array.isArray(choices)) {
873-
discardToolCalls();
874-
yield {
875-
type: "error",
876-
message: "upstream response contained invalid choices",
877-
...(pendingUsage ? { usage: pendingUsage } : {}),
878-
};
879-
return "terminate";
898+
return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
880899
}
881900
if (choices.length === 0) return "continue";
882901
const rawChoice = choices[0];
883902
if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
884-
discardToolCalls();
885-
yield {
886-
type: "error",
887-
message: "upstream response contained invalid choices",
888-
...(pendingUsage ? { usage: pendingUsage } : {}),
889-
};
890-
return "terminate";
903+
return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
891904
}
892905
const choice = rawChoice as {
893906
delta?: Record<string, unknown>;
@@ -897,9 +910,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
897910
if (choice.finish_reason === "error") {
898911
const event = upstreamErrorEvent(choice.error, pendingUsage);
899912
debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
900-
discardToolCalls();
901-
yield event;
902-
return "terminate";
913+
return yield* terminateWithError(event);
903914
}
904915
// Observe the terminator BEFORE the delta guard: a finish-only chunk (finish_reason set,
905916
// no delta) is a graceful close and must record finishReason even though we skip it below.
@@ -908,11 +919,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
908919
}
909920
const delta = choice.delta;
910921
if (delta) {
911-
const reasoningText = typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0
912-
? delta.reasoning_content
913-
: typeof delta.reasoning === "string" && delta.reasoning.length > 0
914-
? delta.reasoning
915-
: undefined;
922+
const reasoningText = reasoningTextFrom(delta);
916923
if (reasoningText !== undefined) {
917924
yield { type: "reasoning_raw_delta", text: reasoningText };
918925
}
@@ -1041,11 +1048,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
10411048
}
10421049
yield* flushToolCalls();
10431050
// Graceful close that omitted [DONE] but delivered finish_reason and/or answer text.
1044-
const stopReason = finishReason === "length"
1045-
? "max_tokens"
1046-
: finishReason === "content_filter"
1047-
? "content_filter"
1048-
: undefined;
1051+
const stopReason = stopReasonFor(finishReason);
10491052
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
10501053
} catch (error) {
10511054
if (isTranslatorBudgetExceededError(error)
@@ -1063,7 +1066,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
10631066
throw error;
10641067
} finally {
10651068
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
1066-
for (const call of pendingToolCalls) budget.closeCall(call.key);
1069+
closeToolCalls();
10671070
reader.releaseLock();
10681071
}
10691072
},
@@ -1097,18 +1100,14 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
10971100
}
10981101
const rawChoice = choices[0];
10991102
if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
1100-
return [{ type: "error", message: "upstream response contained invalid choices", ...(usage ? { usage } : {}) }];
1103+
return [invalidChoicesEvent(usage)];
11011104
}
11021105
const choice = rawChoice;
11031106
if (choice.finish_reason === "error") return [upstreamErrorEvent(choice.error, usage)];
11041107
if (!choice.message) return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
11051108

11061109
const msg = choice.message;
1107-
const reasoningText = typeof msg.reasoning_content === "string" && msg.reasoning_content.length > 0
1108-
? msg.reasoning_content
1109-
: typeof msg.reasoning === "string" && msg.reasoning.length > 0
1110-
? msg.reasoning
1111-
: undefined;
1110+
const reasoningText = reasoningTextFrom(msg);
11121111
if (reasoningText !== undefined) {
11131112
events.push({ type: "reasoning_raw_delta", text: reasoningText });
11141113
}
@@ -1123,11 +1122,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
11231122
events.push({ type: "tool_call_end" });
11241123
}
11251124
}
1126-
const stopReason = choice.finish_reason === "length"
1127-
? "max_tokens"
1128-
: choice.finish_reason === "content_filter"
1129-
? "content_filter"
1130-
: undefined;
1125+
const stopReason = stopReasonFor(choice.finish_reason);
11311126
events.push({
11321127
type: "done",
11331128
usage,

0 commit comments

Comments
 (0)