pi-freeflow 1.26.0 → 1.26.1
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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/commands.ts +76 -8
- package/src/deploy.ts +655 -576
- package/src/proxy.ts +113 -47
- package/src/responses.ts +425 -264
package/src/proxy.ts
CHANGED
|
@@ -58,11 +58,17 @@ import {
|
|
|
58
58
|
issuerRelayFor,
|
|
59
59
|
prepareResponsesFailoverBody,
|
|
60
60
|
rejectedReasoningCount,
|
|
61
|
+
rejectedReasoningIdsCount,
|
|
61
62
|
rememberIssuerRelay,
|
|
62
63
|
rememberRejectedReasoning,
|
|
64
|
+
rememberRejectedReasoningIds,
|
|
63
65
|
responsesConversationKey,
|
|
66
|
+
stripPreviousResponseId,
|
|
64
67
|
stripRejectedReasoning,
|
|
68
|
+
stripRejectedReasoningIds,
|
|
65
69
|
stripReasoningEncryption,
|
|
70
|
+
stripUnresolvableReasoning,
|
|
71
|
+
isExpiredReasoningReference,
|
|
66
72
|
isReasoningCallerMismatch,
|
|
67
73
|
} from "./responses.ts";
|
|
68
74
|
import { pipeUpstreamStream } from "./stream-pipe.ts";
|
|
@@ -730,7 +736,7 @@ function upstreamTimeoutError(): Error & { code: string } {
|
|
|
730
736
|
}
|
|
731
737
|
|
|
732
738
|
/**
|
|
733
|
-
* Recover from upstream rejecting a replayed reasoning
|
|
739
|
+
* Recover from upstream rejecting a replayed reasoning history.
|
|
734
740
|
*
|
|
735
741
|
* Upstream binds each reasoning `encrypted_content` blob to the service
|
|
736
742
|
* instance that issued it. When the conversation is later served by a different
|
|
@@ -740,13 +746,16 @@ function upstreamTimeoutError(): Error & { code: string } {
|
|
|
740
746
|
* second apart, the direct path returned the same 400, and a 45s burst hit ten
|
|
741
747
|
* conversations.
|
|
742
748
|
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
749
|
+
* Reasoning references also age out server-side (observed around 9.5h): the
|
|
750
|
+
* retry after a blob strip can come back as `referenced reasoning item
|
|
751
|
+
* 'rs_...' was not found or has expired`. That poisoned history cannot be
|
|
752
|
+
* resolved by any backend, so the chained second step drops every reasoning
|
|
753
|
+
* item plus `previous_response_id` and retries the same relay again — one
|
|
754
|
+
* turn of reasoning context is lost instead of the whole session.
|
|
747
755
|
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
756
|
+
* Both 400s are request-scoped: the relay served correctly and is never
|
|
757
|
+
* marked failed for them. The 400 body is inspected through a clone: a
|
|
758
|
+
* successful streaming response (the normal path) is never consumed here.
|
|
750
759
|
*/
|
|
751
760
|
async function retryWithoutReasoningEncryption(
|
|
752
761
|
response: Response,
|
|
@@ -765,36 +774,81 @@ async function retryWithoutReasoningEncryption(
|
|
|
765
774
|
} catch {
|
|
766
775
|
return response;
|
|
767
776
|
}
|
|
768
|
-
|
|
777
|
+
const callerMismatch = isReasoningCallerMismatch(errorText);
|
|
778
|
+
const expiredDirect = isExpiredReasoningReference(errorText);
|
|
779
|
+
if (!callerMismatch && !expiredDirect) return response;
|
|
769
780
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
781
|
+
let current = response;
|
|
782
|
+
let currentBody = sentBody;
|
|
783
|
+
let currentErrorText = errorText;
|
|
784
|
+
|
|
785
|
+
if (callerMismatch) {
|
|
786
|
+
const portable = stripReasoningEncryption(sentBody);
|
|
787
|
+
if (!portable) {
|
|
788
|
+
log(
|
|
789
|
+
"warn",
|
|
790
|
+
"upstream rejected caller-bound reasoning but the request carries no encrypted_content to strip",
|
|
791
|
+
{ status: 400 },
|
|
792
|
+
reqId,
|
|
793
|
+
);
|
|
794
|
+
if (!expiredDirect) return response;
|
|
795
|
+
} else {
|
|
796
|
+
log(
|
|
797
|
+
"warn",
|
|
798
|
+
"upstream rejected caller-bound reasoning (blobs issued by another instance); retrying with encrypted_content stripped",
|
|
799
|
+
{ status: 400, tracked: conversationKey !== null },
|
|
800
|
+
reqId,
|
|
801
|
+
);
|
|
802
|
+
const retried = await resend(portable);
|
|
803
|
+
if (conversationKey) {
|
|
804
|
+
const remembered = rememberRejectedReasoning(sentBody, conversationKey);
|
|
805
|
+
log(
|
|
806
|
+
"info",
|
|
807
|
+
`conversation will drop ${remembered} rejected reasoning blob(s) on later turns`,
|
|
808
|
+
undefined,
|
|
809
|
+
reqId,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (retried.status !== 400) return retried;
|
|
813
|
+
if ((retried.headers.get("content-type") ?? "").includes("text/event-stream")) return retried;
|
|
814
|
+
let retryText: string;
|
|
815
|
+
try {
|
|
816
|
+
retryText = await retried.clone().text();
|
|
817
|
+
} catch {
|
|
818
|
+
return retried;
|
|
819
|
+
}
|
|
820
|
+
if (!isExpiredReasoningReference(retryText)) return retried;
|
|
821
|
+
current = retried;
|
|
822
|
+
currentBody = portable;
|
|
823
|
+
currentErrorText = retryText;
|
|
824
|
+
}
|
|
779
825
|
}
|
|
780
826
|
|
|
827
|
+
if (!isExpiredReasoningReference(currentErrorText)) return current;
|
|
828
|
+
const unresolvable = stripUnresolvableReasoning(currentBody);
|
|
829
|
+
if (!unresolvable) {
|
|
830
|
+
if (conversationKey) {
|
|
831
|
+
const remembered = rememberRejectedReasoningIds(currentErrorText, conversationKey);
|
|
832
|
+
log(
|
|
833
|
+
"warn",
|
|
834
|
+
`upstream rejected expired reasoning reference (${remembered} id(s)); nothing strippable left in the request`,
|
|
835
|
+
{ status: 400, tracked: true },
|
|
836
|
+
reqId,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
return current;
|
|
840
|
+
}
|
|
841
|
+
let rememberedIds = 0;
|
|
842
|
+
if (conversationKey) {
|
|
843
|
+
rememberedIds = rememberRejectedReasoningIds(currentErrorText, conversationKey);
|
|
844
|
+
}
|
|
781
845
|
log(
|
|
782
846
|
"warn",
|
|
783
|
-
|
|
847
|
+
`upstream rejected expired reasoning reference (${rememberedIds} id(s)); retrying without reasoning history`,
|
|
784
848
|
{ status: 400, tracked: conversationKey !== null },
|
|
785
849
|
reqId,
|
|
786
850
|
);
|
|
787
|
-
|
|
788
|
-
if (retried.ok && conversationKey) {
|
|
789
|
-
const remembered = rememberRejectedReasoning(sentBody, conversationKey);
|
|
790
|
-
log(
|
|
791
|
-
"info",
|
|
792
|
-
`conversation will drop ${remembered} rejected reasoning blob(s) on later turns`,
|
|
793
|
-
undefined,
|
|
794
|
-
reqId,
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
return retried;
|
|
851
|
+
return await resend(unresolvable);
|
|
798
852
|
}
|
|
799
853
|
|
|
800
854
|
/**
|
|
@@ -1116,16 +1170,23 @@ export function startProxy(
|
|
|
1116
1170
|
// cannot read this history, so send it portable instead of
|
|
1117
1171
|
// letting upstream reject the whole request.
|
|
1118
1172
|
const issuerChanged = issuer !== undefined && issuer !== servingRelay;
|
|
1119
|
-
//
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1173
|
+
// Portable history on hops, selective memory on every turn: dead-id
|
|
1174
|
+
// stripping applies regardless of the hop guard, because a
|
|
1175
|
+
// relay-to-relay or relay-to-direct hop also changes the backend
|
|
1176
|
+
// that must resolve the ids.
|
|
1177
|
+
let bodyForUpstream: Buffer = requestBody;
|
|
1178
|
+
if (responsesRequest && conversationKey !== null) {
|
|
1179
|
+
if (issuerChanged) {
|
|
1180
|
+
bodyForUpstream = stripReasoningEncryption(bodyForUpstream) ?? bodyForUpstream;
|
|
1181
|
+
bodyForUpstream = stripPreviousResponseId(bodyForUpstream) ?? bodyForUpstream;
|
|
1182
|
+
}
|
|
1183
|
+
if (rejectedReasoningCount(conversationKey) > 0) {
|
|
1184
|
+
bodyForUpstream = stripRejectedReasoning(bodyForUpstream, conversationKey) ?? bodyForUpstream;
|
|
1185
|
+
}
|
|
1186
|
+
if (rejectedReasoningIdsCount(conversationKey) > 0) {
|
|
1187
|
+
bodyForUpstream = stripRejectedReasoningIds(bodyForUpstream, conversationKey) ?? bodyForUpstream;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1129
1190
|
if (issuerChanged && conversationKey !== null) {
|
|
1130
1191
|
// These blobs belong to a backend this conversation is
|
|
1131
1192
|
// leaving, so never replay them again.
|
|
@@ -1280,14 +1341,19 @@ export function startProxy(
|
|
|
1280
1341
|
? issuerRelayFor(directConversationKey)
|
|
1281
1342
|
: undefined;
|
|
1282
1343
|
const directIssuerChanged = directIssuer !== undefined && directIssuer !== null;
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1344
|
+
let directBodyForUpstream: Buffer = directBody;
|
|
1345
|
+
if (directResponsesRequest && directConversationKey !== null) {
|
|
1346
|
+
if (directIssuerChanged) {
|
|
1347
|
+
directBodyForUpstream = stripReasoningEncryption(directBodyForUpstream) ?? directBodyForUpstream;
|
|
1348
|
+
directBodyForUpstream = stripPreviousResponseId(directBodyForUpstream) ?? directBodyForUpstream;
|
|
1349
|
+
}
|
|
1350
|
+
if (rejectedReasoningCount(directConversationKey) > 0) {
|
|
1351
|
+
directBodyForUpstream = stripRejectedReasoning(directBodyForUpstream, directConversationKey) ?? directBodyForUpstream;
|
|
1352
|
+
}
|
|
1353
|
+
if (rejectedReasoningIdsCount(directConversationKey) > 0) {
|
|
1354
|
+
directBodyForUpstream = stripRejectedReasoningIds(directBodyForUpstream, directConversationKey) ?? directBodyForUpstream;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1291
1357
|
if (directIssuerChanged && directConversationKey !== null) {
|
|
1292
1358
|
rememberRejectedReasoning(directBody, directConversationKey);
|
|
1293
1359
|
log(
|