thaipass 0.1.2 → 0.1.4
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/dist/index.js +97 -8
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -77,6 +77,7 @@ const ANY_MODELS = [...CHAT_MODELS, ...MEDIA_MODELS];
|
|
|
77
77
|
z.enum(ANY_MODELS, { error: "unknown model, see GET /v1/models" });
|
|
78
78
|
const current = {
|
|
79
79
|
origin: "https://de.aipass.net",
|
|
80
|
+
pricesUrl: "https://openrouter.ai/api/v1/models",
|
|
80
81
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
|
|
81
82
|
};
|
|
82
83
|
const config = current;
|
|
@@ -651,6 +652,21 @@ const toolErrorSchema = (type) => z.object({
|
|
|
651
652
|
toolName: optionalText,
|
|
652
653
|
type: z.literal(type)
|
|
653
654
|
});
|
|
655
|
+
const SWITCH_DETAIL_LIMIT = 120;
|
|
656
|
+
const namedModelSchema = z.object({
|
|
657
|
+
model: optionalText,
|
|
658
|
+
modelId: optionalText,
|
|
659
|
+
to: optionalText
|
|
660
|
+
});
|
|
661
|
+
/** The payload is undocumented, so an unnamed one is kept verbatim for the first production sample to show. */
|
|
662
|
+
const switchDetailSchema = z.unknown().transform((data) => {
|
|
663
|
+
const named = namedModelSchema.safeParse(data);
|
|
664
|
+
if (named.success) {
|
|
665
|
+
const found = named.data.modelId ?? named.data.model ?? named.data.to;
|
|
666
|
+
if (found !== void 0 && found.length > 0) return found;
|
|
667
|
+
}
|
|
668
|
+
return JSON.stringify(data ?? null).slice(0, SWITCH_DETAIL_LIMIT);
|
|
669
|
+
});
|
|
654
670
|
const upstreamEventSchema = z.discriminatedUnion("type", [
|
|
655
671
|
z.object({
|
|
656
672
|
delta: z.string(),
|
|
@@ -660,6 +676,10 @@ const upstreamEventSchema = z.discriminatedUnion("type", [
|
|
|
660
676
|
delta: z.string(),
|
|
661
677
|
type: z.literal("reasoning-delta")
|
|
662
678
|
}),
|
|
679
|
+
z.object({
|
|
680
|
+
data: switchDetailSchema,
|
|
681
|
+
type: z.literal("data-model_switched")
|
|
682
|
+
}),
|
|
663
683
|
fileFrameSchema,
|
|
664
684
|
z.object({
|
|
665
685
|
finishReason: optionalText,
|
|
@@ -711,6 +731,12 @@ const parseAipassSSE = async function* parseAipassSSE(body, skips) {
|
|
|
711
731
|
text: event.delta
|
|
712
732
|
};
|
|
713
733
|
break;
|
|
734
|
+
case "data-model_switched":
|
|
735
|
+
yield {
|
|
736
|
+
detail: event.data,
|
|
737
|
+
kind: "switch"
|
|
738
|
+
};
|
|
739
|
+
break;
|
|
714
740
|
case "file":
|
|
715
741
|
yield {
|
|
716
742
|
file: toFileEvent(event),
|
|
@@ -724,12 +750,15 @@ const parseAipassSSE = async function* parseAipassSSE(body, skips) {
|
|
|
724
750
|
};
|
|
725
751
|
break;
|
|
726
752
|
case "tool-input-error":
|
|
727
|
-
case "tool-output-error":
|
|
753
|
+
case "tool-output-error": {
|
|
754
|
+
const tool = event.toolName ?? "a tool";
|
|
728
755
|
yield {
|
|
729
756
|
kind: "error",
|
|
730
|
-
message: `upstream failed on ${
|
|
757
|
+
message: `upstream failed on ${tool}: ${event.errorText ?? "no detail"}`,
|
|
758
|
+
tool
|
|
731
759
|
};
|
|
732
760
|
break;
|
|
761
|
+
}
|
|
733
762
|
default: yield {
|
|
734
763
|
kind: "error",
|
|
735
764
|
message: event.errorText ?? event.error ?? "upstream error"
|
|
@@ -815,9 +844,47 @@ const heldLength = (tail) => {
|
|
|
815
844
|
for (let length = longest; length > 0; length -= 1) if ("```tool_call\n".startsWith(tail.slice(-length))) return length;
|
|
816
845
|
return 0;
|
|
817
846
|
};
|
|
847
|
+
const UNFENCED_LIMIT = 4096;
|
|
848
|
+
const objectEnd = (value, from) => {
|
|
849
|
+
let depth = 0;
|
|
850
|
+
let inString = false;
|
|
851
|
+
let escaped = false;
|
|
852
|
+
for (let index = from; index < value.length; index += 1) {
|
|
853
|
+
const char = value[index];
|
|
854
|
+
if (inString) {
|
|
855
|
+
if (escaped) escaped = false;
|
|
856
|
+
else if (char === "\\") escaped = true;
|
|
857
|
+
else if (char === "\"") inString = false;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (char === "\"") inString = true;
|
|
861
|
+
else if (char === "{") depth += 1;
|
|
862
|
+
else if (char === "}") {
|
|
863
|
+
depth -= 1;
|
|
864
|
+
if (depth === 0) return index + 1;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return -1;
|
|
868
|
+
};
|
|
818
869
|
/**
|
|
819
870
|
* Text is released as soon as it can no longer open a fence. A fence that
|
|
820
871
|
* never closes, or holds no call to an offered tool, is released as text.
|
|
872
|
+
*
|
|
873
|
+
* A reply that opens with the call object and no fence is read as a call
|
|
874
|
+
* anyway. Some models write the body the guide asks for and drop the fence
|
|
875
|
+
* around it, and the reply then arrives as prose with `stop`: observed as
|
|
876
|
+
* `{"name":"load_skill","input":{…}}I can begin once the load_skill tool
|
|
877
|
+
* result is available.` The caller sees a finished answer that narrates a
|
|
878
|
+
* call nobody ran, which is worse than an error, and the retry in
|
|
879
|
+
* `reply.ts` cannot see it either, since that waits for a `tool-calls`
|
|
880
|
+
* finish reason. Recovering it here serves a streaming caller too, which
|
|
881
|
+
* cannot be retried at all.
|
|
882
|
+
*
|
|
883
|
+
* The rule is deliberately the narrowest one that covers what was observed:
|
|
884
|
+
* the object must be the first thing in the reply, it must parse, and it
|
|
885
|
+
* must name a tool the caller offered — the same bar a fenced block passes.
|
|
886
|
+
* Prose that quotes a call later on is left alone, which matters when the
|
|
887
|
+
* caller is reviewing code and may quote one on purpose.
|
|
821
888
|
*/
|
|
822
889
|
const splitReply = (tools) => {
|
|
823
890
|
if (tools.length === 0) return {
|
|
@@ -827,6 +894,7 @@ const splitReply = (tools) => {
|
|
|
827
894
|
const names = new Set(tools.map((tool) => tool.name));
|
|
828
895
|
let buffer = "";
|
|
829
896
|
let inBlock = false;
|
|
897
|
+
let opening = true;
|
|
830
898
|
/** Ends the open block; `fence` is what closed it, nothing when the reply ran out first. */
|
|
831
899
|
const closeBlock = (json, fence) => {
|
|
832
900
|
inBlock = false;
|
|
@@ -836,9 +904,31 @@ const splitReply = (tools) => {
|
|
|
836
904
|
type: "call"
|
|
837
905
|
}] : text(`${FENCE_OPEN}${json}${fence}`);
|
|
838
906
|
};
|
|
907
|
+
const openingCall = (final) => {
|
|
908
|
+
const start = buffer.length - buffer.trimStart().length;
|
|
909
|
+
if (buffer[start] !== "{") return buffer.trim().length === 0 && !final ? "hold" : void 0;
|
|
910
|
+
const end = objectEnd(buffer, start);
|
|
911
|
+
if (end === -1) return !final && buffer.length - start < UNFENCED_LIMIT ? "hold" : void 0;
|
|
912
|
+
const call = parseCall(buffer.slice(start, end), names);
|
|
913
|
+
if (call === void 0) return;
|
|
914
|
+
buffer = buffer.slice(end);
|
|
915
|
+
return call;
|
|
916
|
+
};
|
|
839
917
|
const drain = (final) => {
|
|
840
918
|
const parts = [];
|
|
841
919
|
for (;;) {
|
|
920
|
+
if (opening && !inBlock) {
|
|
921
|
+
const found = openingCall(final);
|
|
922
|
+
if (found === "hold") return parts;
|
|
923
|
+
opening = false;
|
|
924
|
+
if (found !== void 0) {
|
|
925
|
+
parts.push({
|
|
926
|
+
call: found,
|
|
927
|
+
type: "call"
|
|
928
|
+
});
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
842
932
|
if (inBlock) {
|
|
843
933
|
const end = buffer.indexOf(FENCE_CLOSE);
|
|
844
934
|
if (end === -1) {
|
|
@@ -971,7 +1061,8 @@ const readReply = (options) => {
|
|
|
971
1061
|
skips: {
|
|
972
1062
|
count: 0,
|
|
973
1063
|
types: /* @__PURE__ */ new Set()
|
|
974
|
-
}
|
|
1064
|
+
},
|
|
1065
|
+
switchedModel: void 0
|
|
975
1066
|
};
|
|
976
1067
|
const splitter = splitReply(tools);
|
|
977
1068
|
const fromParts = function* fromParts(parts) {
|
|
@@ -1003,7 +1094,8 @@ const readReply = (options) => {
|
|
|
1003
1094
|
kind: "reasoning",
|
|
1004
1095
|
text: event.text
|
|
1005
1096
|
};
|
|
1006
|
-
} else if (event.kind === "
|
|
1097
|
+
} else if (event.kind === "switch") tally.switchedModel = event.detail;
|
|
1098
|
+
else if (event.kind === "file") {
|
|
1007
1099
|
const asset = await resolveAsset(cookie, event.file, signal);
|
|
1008
1100
|
if (asset) {
|
|
1009
1101
|
tally.files += 1;
|
|
@@ -1013,10 +1105,7 @@ const readReply = (options) => {
|
|
|
1013
1105
|
};
|
|
1014
1106
|
}
|
|
1015
1107
|
} else if (event.kind === "finish") tally.finishReason = event.reason;
|
|
1016
|
-
else yield
|
|
1017
|
-
kind: "error",
|
|
1018
|
-
message: event.message
|
|
1019
|
-
};
|
|
1108
|
+
else yield event;
|
|
1020
1109
|
yield* fromParts(splitter.flush());
|
|
1021
1110
|
}(),
|
|
1022
1111
|
tally
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thaipass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "AI SDK provider for AI Pass, using your own session cookie.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
@@ -42,8 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@thaipass/core": "workspace:*",
|
|
45
|
-
"@
|
|
46
|
-
"@types/bun": "1.4.0",
|
|
45
|
+
"@types/bun": "1.4.1",
|
|
47
46
|
"tsdown": "0.23.0",
|
|
48
47
|
"typescript": "7.0.2"
|
|
49
48
|
}
|