thaipass 0.1.2 → 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.
Files changed (2) hide show
  1. package/dist/index.js +68 -6
  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;
@@ -724,12 +725,15 @@ const parseAipassSSE = async function* parseAipassSSE(body, skips) {
724
725
  };
725
726
  break;
726
727
  case "tool-input-error":
727
- case "tool-output-error":
728
+ case "tool-output-error": {
729
+ const tool = event.toolName ?? "a tool";
728
730
  yield {
729
731
  kind: "error",
730
- message: `upstream failed on ${event.toolName ?? "a tool"}: ${event.errorText ?? "no detail"}`
732
+ message: `upstream failed on ${tool}: ${event.errorText ?? "no detail"}`,
733
+ tool
731
734
  };
732
735
  break;
736
+ }
733
737
  default: yield {
734
738
  kind: "error",
735
739
  message: event.errorText ?? event.error ?? "upstream error"
@@ -815,9 +819,47 @@ const heldLength = (tail) => {
815
819
  for (let length = longest; length > 0; length -= 1) if ("```tool_call\n".startsWith(tail.slice(-length))) return length;
816
820
  return 0;
817
821
  };
822
+ const UNFENCED_LIMIT = 4096;
823
+ const objectEnd = (value, from) => {
824
+ let depth = 0;
825
+ let inString = false;
826
+ let escaped = false;
827
+ for (let index = from; index < value.length; index += 1) {
828
+ const char = value[index];
829
+ if (inString) {
830
+ if (escaped) escaped = false;
831
+ else if (char === "\\") escaped = true;
832
+ else if (char === "\"") inString = false;
833
+ continue;
834
+ }
835
+ if (char === "\"") inString = true;
836
+ else if (char === "{") depth += 1;
837
+ else if (char === "}") {
838
+ depth -= 1;
839
+ if (depth === 0) return index + 1;
840
+ }
841
+ }
842
+ return -1;
843
+ };
818
844
  /**
819
845
  * Text is released as soon as it can no longer open a fence. A fence that
820
846
  * never closes, or holds no call to an offered tool, is released as text.
847
+ *
848
+ * A reply that opens with the call object and no fence is read as a call
849
+ * anyway. Some models write the body the guide asks for and drop the fence
850
+ * around it, and the reply then arrives as prose with `stop`: observed as
851
+ * `{"name":"load_skill","input":{…}}I can begin once the load_skill tool
852
+ * result is available.` The caller sees a finished answer that narrates a
853
+ * call nobody ran, which is worse than an error, and the retry in
854
+ * `reply.ts` cannot see it either, since that waits for a `tool-calls`
855
+ * finish reason. Recovering it here serves a streaming caller too, which
856
+ * cannot be retried at all.
857
+ *
858
+ * The rule is deliberately the narrowest one that covers what was observed:
859
+ * the object must be the first thing in the reply, it must parse, and it
860
+ * must name a tool the caller offered — the same bar a fenced block passes.
861
+ * Prose that quotes a call later on is left alone, which matters when the
862
+ * caller is reviewing code and may quote one on purpose.
821
863
  */
822
864
  const splitReply = (tools) => {
823
865
  if (tools.length === 0) return {
@@ -827,6 +869,7 @@ const splitReply = (tools) => {
827
869
  const names = new Set(tools.map((tool) => tool.name));
828
870
  let buffer = "";
829
871
  let inBlock = false;
872
+ let opening = true;
830
873
  /** Ends the open block; `fence` is what closed it, nothing when the reply ran out first. */
831
874
  const closeBlock = (json, fence) => {
832
875
  inBlock = false;
@@ -836,9 +879,31 @@ const splitReply = (tools) => {
836
879
  type: "call"
837
880
  }] : text(`${FENCE_OPEN}${json}${fence}`);
838
881
  };
882
+ const openingCall = (final) => {
883
+ const start = buffer.length - buffer.trimStart().length;
884
+ if (buffer[start] !== "{") return buffer.trim().length === 0 && !final ? "hold" : void 0;
885
+ const end = objectEnd(buffer, start);
886
+ if (end === -1) return !final && buffer.length - start < UNFENCED_LIMIT ? "hold" : void 0;
887
+ const call = parseCall(buffer.slice(start, end), names);
888
+ if (call === void 0) return;
889
+ buffer = buffer.slice(end);
890
+ return call;
891
+ };
839
892
  const drain = (final) => {
840
893
  const parts = [];
841
894
  for (;;) {
895
+ if (opening && !inBlock) {
896
+ const found = openingCall(final);
897
+ if (found === "hold") return parts;
898
+ opening = false;
899
+ if (found !== void 0) {
900
+ parts.push({
901
+ call: found,
902
+ type: "call"
903
+ });
904
+ continue;
905
+ }
906
+ }
842
907
  if (inBlock) {
843
908
  const end = buffer.indexOf(FENCE_CLOSE);
844
909
  if (end === -1) {
@@ -1013,10 +1078,7 @@ const readReply = (options) => {
1013
1078
  };
1014
1079
  }
1015
1080
  } else if (event.kind === "finish") tally.finishReason = event.reason;
1016
- else yield {
1017
- kind: "error",
1018
- message: event.message
1019
- };
1081
+ else yield event;
1020
1082
  yield* fromParts(splitter.flush());
1021
1083
  }(),
1022
1084
  tally
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thaipass",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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
  }