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.
Files changed (2) hide show
  1. package/dist/index.js +97 -8
  2. 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 ${event.toolName ?? "a tool"}: ${event.errorText ?? "no detail"}`
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 === "file") {
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.2",
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
- "@thaipass/typescript-config": "workspace:*",
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
  }