@punktechnologies/sdk 0.1.2 → 0.2.0
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/README.md +81 -16
- package/dist/index.d.ts +170 -3
- package/dist/index.js +471 -7
- package/package.json +1 -1
- package/src/index.ts +636 -8
package/dist/index.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
var PUNK_CHORUS_MODEL = "punk/chorus";
|
|
3
3
|
var PUNK_MODEL = PUNK_CHORUS_MODEL;
|
|
4
4
|
var DEFAULT_BASE_URL = "http://localhost:4100";
|
|
5
|
+
var DEFAULT_APP = "default-app";
|
|
5
6
|
var MAX_TOOL_CACHE_TTL_SECONDS = 24 * 60 * 60;
|
|
6
7
|
var MAX_TOOL_CACHE_DIMENSION_CHARS = 120;
|
|
8
|
+
var DEFAULT_WAIT_POLL_INTERVAL_MS = 250;
|
|
9
|
+
var DEFAULT_WAIT_TIMEOUT_MS = 30000;
|
|
7
10
|
function stringifyToolArguments(value) {
|
|
8
11
|
if (typeof value === "string")
|
|
9
12
|
return value;
|
|
@@ -467,6 +470,16 @@ function cleanPathId(label, value) {
|
|
|
467
470
|
}
|
|
468
471
|
return trimmed;
|
|
469
472
|
}
|
|
473
|
+
function readEnv(name) {
|
|
474
|
+
const env = globalThis?.process?.env;
|
|
475
|
+
if (!env || typeof env !== "object")
|
|
476
|
+
return;
|
|
477
|
+
const value = env[name];
|
|
478
|
+
return typeof value === "string" ? value : undefined;
|
|
479
|
+
}
|
|
480
|
+
function optionOrEnv(opts, key, envName) {
|
|
481
|
+
return Object.prototype.hasOwnProperty.call(opts, key) ? opts[key] : readEnv(envName);
|
|
482
|
+
}
|
|
470
483
|
function normalizeBaseUrl(value) {
|
|
471
484
|
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
472
485
|
return (trimmed || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -513,6 +526,170 @@ function chatMessageText(message) {
|
|
|
513
526
|
}
|
|
514
527
|
return chatMessageContentToString(message?.content);
|
|
515
528
|
}
|
|
529
|
+
function finiteNumber(value) {
|
|
530
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
531
|
+
}
|
|
532
|
+
function normalizeOpenAIUsage(raw) {
|
|
533
|
+
if (!isRecord(raw?.usage))
|
|
534
|
+
return;
|
|
535
|
+
const usage = raw.usage;
|
|
536
|
+
const normalized = { raw: raw.usage };
|
|
537
|
+
const inputTokens = finiteNumber(usage.prompt_tokens ?? usage.input_tokens);
|
|
538
|
+
const outputTokens = finiteNumber(usage.completion_tokens ?? usage.output_tokens);
|
|
539
|
+
const totalTokens = finiteNumber(usage.total_tokens);
|
|
540
|
+
if (inputTokens !== undefined)
|
|
541
|
+
normalized.inputTokens = inputTokens;
|
|
542
|
+
if (outputTokens !== undefined)
|
|
543
|
+
normalized.outputTokens = outputTokens;
|
|
544
|
+
if (totalTokens !== undefined)
|
|
545
|
+
normalized.totalTokens = totalTokens;
|
|
546
|
+
if (normalized.totalTokens === undefined && inputTokens !== undefined && outputTokens !== undefined) {
|
|
547
|
+
normalized.totalTokens = inputTokens + outputTokens;
|
|
548
|
+
}
|
|
549
|
+
return normalized;
|
|
550
|
+
}
|
|
551
|
+
function normalizeAnthropicUsage(raw) {
|
|
552
|
+
if (!isRecord(raw?.usage))
|
|
553
|
+
return;
|
|
554
|
+
const usage = raw.usage;
|
|
555
|
+
const normalized = { raw: raw.usage };
|
|
556
|
+
const inputTokens = finiteNumber(usage.input_tokens);
|
|
557
|
+
const outputTokens = finiteNumber(usage.output_tokens);
|
|
558
|
+
if (inputTokens !== undefined)
|
|
559
|
+
normalized.inputTokens = inputTokens;
|
|
560
|
+
if (outputTokens !== undefined)
|
|
561
|
+
normalized.outputTokens = outputTokens;
|
|
562
|
+
if (inputTokens !== undefined && outputTokens !== undefined)
|
|
563
|
+
normalized.totalTokens = inputTokens + outputTokens;
|
|
564
|
+
return normalized;
|
|
565
|
+
}
|
|
566
|
+
function normalizeModel(raw) {
|
|
567
|
+
return typeof raw?.model === "string" && raw.model.length > 0 ? raw.model : undefined;
|
|
568
|
+
}
|
|
569
|
+
function normalizeProvider(raw) {
|
|
570
|
+
for (const key of ["provider", "providerName", "provider_name"]) {
|
|
571
|
+
if (typeof raw?.[key] === "string" && raw[key].length > 0)
|
|
572
|
+
return raw[key];
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
function anthropicTextFromContent(value, method, path) {
|
|
577
|
+
if (typeof value === "string")
|
|
578
|
+
return value;
|
|
579
|
+
if (!Array.isArray(value)) {
|
|
580
|
+
throw malformedApiResponse(method, path, "content must be a string or content block array");
|
|
581
|
+
}
|
|
582
|
+
return value.map((part) => {
|
|
583
|
+
if (typeof part === "string")
|
|
584
|
+
return part;
|
|
585
|
+
if (part?.type === "text" && typeof part.text === "string")
|
|
586
|
+
return part.text;
|
|
587
|
+
return "";
|
|
588
|
+
}).join("");
|
|
589
|
+
}
|
|
590
|
+
function anthropicMessageText(raw, method, path) {
|
|
591
|
+
if (!isRecord(raw))
|
|
592
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
593
|
+
if (!Object.prototype.hasOwnProperty.call(raw, "content")) {
|
|
594
|
+
throw malformedApiResponse(method, path, "missing content");
|
|
595
|
+
}
|
|
596
|
+
return anthropicTextFromContent(raw.content, method, path);
|
|
597
|
+
}
|
|
598
|
+
function anthropicContentBlocks(raw, method, path) {
|
|
599
|
+
if (!isRecord(raw))
|
|
600
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
601
|
+
const content = raw.content;
|
|
602
|
+
if (typeof content === "string")
|
|
603
|
+
return [{ type: "text", text: content }];
|
|
604
|
+
if (!Array.isArray(content))
|
|
605
|
+
throw malformedApiResponse(method, path, "content must be a string or content block array");
|
|
606
|
+
return content.filter(isRecord);
|
|
607
|
+
}
|
|
608
|
+
function parseSseFrame(frame) {
|
|
609
|
+
const data = [];
|
|
610
|
+
let event;
|
|
611
|
+
for (const line of frame.split(/\r?\n/)) {
|
|
612
|
+
if (line.length === 0 || line.startsWith(":"))
|
|
613
|
+
continue;
|
|
614
|
+
if (line.startsWith("event:")) {
|
|
615
|
+
event = line.slice("event:".length).trim();
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (line.startsWith("data:")) {
|
|
619
|
+
const value = line.slice("data:".length);
|
|
620
|
+
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (data.length === 0)
|
|
624
|
+
return null;
|
|
625
|
+
return { event, data: data.join(`
|
|
626
|
+
`) };
|
|
627
|
+
}
|
|
628
|
+
async function* sseFrames(res, method, path) {
|
|
629
|
+
if (!res.body) {
|
|
630
|
+
throw new Error(`Punk API ${method} ${path} returned an empty stream`);
|
|
631
|
+
}
|
|
632
|
+
const reader = res.body.getReader();
|
|
633
|
+
const decoder = new TextDecoder;
|
|
634
|
+
let buffer = "";
|
|
635
|
+
try {
|
|
636
|
+
while (true) {
|
|
637
|
+
const { done, value } = await reader.read();
|
|
638
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
639
|
+
let boundary = buffer.search(/\r?\n\r?\n/);
|
|
640
|
+
while (boundary >= 0) {
|
|
641
|
+
const frameText = buffer.slice(0, boundary);
|
|
642
|
+
const match = buffer.slice(boundary).match(/^\r?\n\r?\n/);
|
|
643
|
+
buffer = buffer.slice(boundary + (match?.[0].length ?? 2));
|
|
644
|
+
const frame = parseSseFrame(frameText);
|
|
645
|
+
if (frame)
|
|
646
|
+
yield frame;
|
|
647
|
+
boundary = buffer.search(/\r?\n\r?\n/);
|
|
648
|
+
}
|
|
649
|
+
if (done)
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
const tail = parseSseFrame(buffer.trim());
|
|
653
|
+
if (tail)
|
|
654
|
+
yield tail;
|
|
655
|
+
} finally {
|
|
656
|
+
reader.releaseLock();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function parseSseJson(frame, method, path) {
|
|
660
|
+
try {
|
|
661
|
+
return JSON.parse(frame.data);
|
|
662
|
+
} catch {
|
|
663
|
+
throw malformedApiResponse(method, path, "stream data was not JSON");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function streamDeltaText(value) {
|
|
667
|
+
if (value === undefined || value === null)
|
|
668
|
+
return "";
|
|
669
|
+
if (typeof value === "string")
|
|
670
|
+
return value;
|
|
671
|
+
if (Array.isArray(value)) {
|
|
672
|
+
return value.map((part) => typeof part?.text === "string" ? part.text : "").join("");
|
|
673
|
+
}
|
|
674
|
+
return "";
|
|
675
|
+
}
|
|
676
|
+
function isTerminalRunStatus(status) {
|
|
677
|
+
return status === "completed" || status === "failed" || status === "blocked";
|
|
678
|
+
}
|
|
679
|
+
function checkedDelay(ms, signal) {
|
|
680
|
+
if (signal?.aborted)
|
|
681
|
+
return Promise.reject(new Error("Punk wait aborted"));
|
|
682
|
+
return new Promise((resolve, reject) => {
|
|
683
|
+
const timeout = setTimeout(resolve, ms);
|
|
684
|
+
const onAbort = () => {
|
|
685
|
+
clearTimeout(timeout);
|
|
686
|
+
reject(new Error("Punk wait aborted"));
|
|
687
|
+
};
|
|
688
|
+
if (signal) {
|
|
689
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|
|
516
693
|
var TOOL_LEVEL_4 = new Set([
|
|
517
694
|
"delete",
|
|
518
695
|
"remove",
|
|
@@ -687,12 +864,107 @@ class Punk {
|
|
|
687
864
|
agent;
|
|
688
865
|
subject;
|
|
689
866
|
apiKey;
|
|
867
|
+
runStack = [];
|
|
690
868
|
constructor(opts = {}) {
|
|
691
|
-
this.baseUrl = normalizeBaseUrl(opts
|
|
692
|
-
this.apiKey = cleanIdentityValue(opts
|
|
693
|
-
this.app = cleanIdentityValue(opts
|
|
694
|
-
this.agent = cleanIdentityValue(opts
|
|
695
|
-
this.subject = cleanIdentityValue(opts
|
|
869
|
+
this.baseUrl = normalizeBaseUrl(optionOrEnv(opts, "baseUrl", "PUNK_BASE_URL"));
|
|
870
|
+
this.apiKey = cleanIdentityValue(optionOrEnv(opts, "apiKey", "PUNK_API_KEY"));
|
|
871
|
+
this.app = cleanIdentityValue(optionOrEnv(opts, "app", "PUNK_APP")) ?? DEFAULT_APP;
|
|
872
|
+
this.agent = cleanIdentityValue(optionOrEnv(opts, "agent", "PUNK_AGENT"));
|
|
873
|
+
this.subject = cleanIdentityValue(optionOrEnv(opts, "subject", "PUNK_SUBJECT"));
|
|
874
|
+
}
|
|
875
|
+
gateway = {
|
|
876
|
+
chat: (params) => this.chat(params),
|
|
877
|
+
streamChat: (params) => this.streamChat(params),
|
|
878
|
+
stream: (params) => this.chatStream(params)
|
|
879
|
+
};
|
|
880
|
+
openai = {
|
|
881
|
+
chat: (params) => this.chat(params),
|
|
882
|
+
streamChat: (params) => this.streamChat(params),
|
|
883
|
+
stream: (params) => this.chatStream(params),
|
|
884
|
+
config: (opts) => this.openAIConfig(opts)
|
|
885
|
+
};
|
|
886
|
+
anthropic = {
|
|
887
|
+
messages: (params) => this.messages(params),
|
|
888
|
+
streamMessages: (params) => this.streamMessages(params),
|
|
889
|
+
stream: (params) => this.messagesStream(params),
|
|
890
|
+
config: (opts) => this.anthropicConfig(opts)
|
|
891
|
+
};
|
|
892
|
+
identityHeaders(opts = {}) {
|
|
893
|
+
const includeAuthorization = opts.includeAuthorization !== false;
|
|
894
|
+
const headers = {};
|
|
895
|
+
if (opts.includeContentType === true)
|
|
896
|
+
headers["Content-Type"] = "application/json";
|
|
897
|
+
if (includeAuthorization && this.apiKey)
|
|
898
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
899
|
+
const app = cleanIdentityValue(opts.app) ?? this.app;
|
|
900
|
+
const agent = cleanIdentityValue(opts.agent) ?? this.agent;
|
|
901
|
+
const subject = cleanIdentityValue(opts.subject) ?? this.subject;
|
|
902
|
+
headers["X-Punk-App"] = app;
|
|
903
|
+
if (agent)
|
|
904
|
+
headers["X-Punk-Agent"] = agent;
|
|
905
|
+
if (subject)
|
|
906
|
+
headers["X-Punk-Subject"] = subject;
|
|
907
|
+
if (opts.headers) {
|
|
908
|
+
for (const [key, value] of Object.entries(opts.headers)) {
|
|
909
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
910
|
+
headers[key] = value;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return headers;
|
|
914
|
+
}
|
|
915
|
+
openAIConfig(opts = {}) {
|
|
916
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl ?? this.baseUrl);
|
|
917
|
+
const apiKey = cleanIdentityValue(opts.apiKey) ?? this.apiKey ?? "punk-local";
|
|
918
|
+
return {
|
|
919
|
+
baseURL: `${baseUrl}/v1`,
|
|
920
|
+
apiKey,
|
|
921
|
+
defaultHeaders: this.identityHeaders({ ...opts, includeAuthorization: false })
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
anthropicConfig(opts = {}) {
|
|
925
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl ?? this.baseUrl);
|
|
926
|
+
const authToken = cleanIdentityValue(opts.apiKey) ?? this.apiKey ?? "punk-local";
|
|
927
|
+
return {
|
|
928
|
+
baseURL: baseUrl,
|
|
929
|
+
authToken,
|
|
930
|
+
defaultHeaders: this.identityHeaders({ ...opts, includeAuthorization: true })
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
vercelAIConfig(opts = {}) {
|
|
934
|
+
return this.vercelOpenAICompatibleConfig(opts);
|
|
935
|
+
}
|
|
936
|
+
vercelOpenAICompatibleConfig(opts = {}) {
|
|
937
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl ?? this.baseUrl);
|
|
938
|
+
return {
|
|
939
|
+
name: cleanIdentityValue(opts.name) ?? "punk",
|
|
940
|
+
baseURL: `${baseUrl}/v1`,
|
|
941
|
+
apiKey: cleanIdentityValue(opts.apiKey) ?? this.apiKey ?? "punk-local",
|
|
942
|
+
headers: this.identityHeaders({ ...opts, includeAuthorization: false }),
|
|
943
|
+
includeUsage: opts.includeUsage ?? true
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
langChainConfig(opts = {}) {
|
|
947
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl ?? this.baseUrl);
|
|
948
|
+
return {
|
|
949
|
+
apiKey: cleanIdentityValue(opts.apiKey) ?? this.apiKey ?? "punk-local",
|
|
950
|
+
configuration: {
|
|
951
|
+
baseURL: `${baseUrl}/v1`,
|
|
952
|
+
defaultHeaders: this.identityHeaders({ ...opts, includeAuthorization: false })
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
currentRunId() {
|
|
957
|
+
return this.runStack.at(-1);
|
|
958
|
+
}
|
|
959
|
+
async withRun(run, fn) {
|
|
960
|
+
const rawRunId = typeof run === "string" ? run : run.runId;
|
|
961
|
+
const id = cleanPathId("run id", rawRunId ?? "");
|
|
962
|
+
this.runStack.push(id);
|
|
963
|
+
try {
|
|
964
|
+
return await fn();
|
|
965
|
+
} finally {
|
|
966
|
+
this.runStack.pop();
|
|
967
|
+
}
|
|
696
968
|
}
|
|
697
969
|
async chat(params) {
|
|
698
970
|
let res;
|
|
@@ -719,16 +991,163 @@ class Punk {
|
|
|
719
991
|
toolCalls,
|
|
720
992
|
runId: res.headers.get("x-punk-run-id") ?? "",
|
|
721
993
|
route: res.headers.get("x-punk-route") ?? "unknown",
|
|
722
|
-
raw
|
|
994
|
+
raw,
|
|
995
|
+
usage: normalizeOpenAIUsage(raw),
|
|
996
|
+
model: normalizeModel(raw),
|
|
997
|
+
provider: normalizeProvider(raw)
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
streamChat(params) {
|
|
1001
|
+
return this.chatStream(params);
|
|
1002
|
+
}
|
|
1003
|
+
async* chatStream(params) {
|
|
1004
|
+
const method = "POST";
|
|
1005
|
+
const path = "/v1/chat/completions";
|
|
1006
|
+
let res;
|
|
1007
|
+
try {
|
|
1008
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
1009
|
+
method,
|
|
1010
|
+
headers: this.headers({
|
|
1011
|
+
"X-Punk-App": this.app,
|
|
1012
|
+
"X-Punk-Agent": this.agent,
|
|
1013
|
+
"X-Punk-Subject": this.subject
|
|
1014
|
+
}),
|
|
1015
|
+
body: JSON.stringify({ ...params, stream: true })
|
|
1016
|
+
});
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
throw this.toTransportError(method, path, err);
|
|
1019
|
+
}
|
|
1020
|
+
if (!res.ok)
|
|
1021
|
+
throw this.toError(method, path, res, await res.text());
|
|
1022
|
+
const runId = res.headers.get("x-punk-run-id") ?? "";
|
|
1023
|
+
const route = res.headers.get("x-punk-route") ?? "unknown";
|
|
1024
|
+
for await (const frame of sseFrames(res, method, path)) {
|
|
1025
|
+
if (frame.data === "[DONE]") {
|
|
1026
|
+
yield { type: "done", content: "", toolCalls: [], runId, route };
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const raw = parseSseJson(frame, method, path);
|
|
1030
|
+
const choice = Array.isArray(raw?.choices) ? raw.choices[0] : undefined;
|
|
1031
|
+
const delta = isRecord(choice?.delta) ? choice.delta : {};
|
|
1032
|
+
yield {
|
|
1033
|
+
type: "delta",
|
|
1034
|
+
content: streamDeltaText(delta.content),
|
|
1035
|
+
toolCalls: normalizeChatToolCalls(delta.tool_calls),
|
|
1036
|
+
runId,
|
|
1037
|
+
route,
|
|
1038
|
+
raw,
|
|
1039
|
+
usage: normalizeOpenAIUsage(raw),
|
|
1040
|
+
model: normalizeModel(raw),
|
|
1041
|
+
provider: normalizeProvider(raw)
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
yield { type: "done", content: "", toolCalls: [], runId, route };
|
|
1045
|
+
}
|
|
1046
|
+
async messages(params) {
|
|
1047
|
+
const method = "POST";
|
|
1048
|
+
const path = "/v1/messages";
|
|
1049
|
+
let res;
|
|
1050
|
+
try {
|
|
1051
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
1052
|
+
method,
|
|
1053
|
+
headers: this.headers({
|
|
1054
|
+
"anthropic-version": "2023-06-01",
|
|
1055
|
+
"X-Punk-App": this.app,
|
|
1056
|
+
"X-Punk-Agent": this.agent,
|
|
1057
|
+
"X-Punk-Subject": this.subject
|
|
1058
|
+
}),
|
|
1059
|
+
body: JSON.stringify({ ...params, stream: false })
|
|
1060
|
+
});
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
throw this.toTransportError(method, path, err);
|
|
1063
|
+
}
|
|
1064
|
+
const raw = await this.parseJsonResponse(method, path, res);
|
|
1065
|
+
return {
|
|
1066
|
+
content: anthropicMessageText(raw, method, path),
|
|
1067
|
+
contentBlocks: anthropicContentBlocks(raw, method, path),
|
|
1068
|
+
runId: res.headers.get("x-punk-run-id") ?? "",
|
|
1069
|
+
route: res.headers.get("x-punk-route") ?? "unknown",
|
|
1070
|
+
raw,
|
|
1071
|
+
usage: normalizeAnthropicUsage(raw),
|
|
1072
|
+
model: normalizeModel(raw),
|
|
1073
|
+
provider: normalizeProvider(raw),
|
|
1074
|
+
stopReason: typeof raw?.stop_reason === "string" ? raw.stop_reason : undefined
|
|
723
1075
|
};
|
|
724
1076
|
}
|
|
1077
|
+
streamMessages(params) {
|
|
1078
|
+
return this.messagesStream(params);
|
|
1079
|
+
}
|
|
1080
|
+
async* messagesStream(params) {
|
|
1081
|
+
const method = "POST";
|
|
1082
|
+
const path = "/v1/messages";
|
|
1083
|
+
let res;
|
|
1084
|
+
try {
|
|
1085
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
1086
|
+
method,
|
|
1087
|
+
headers: this.headers({
|
|
1088
|
+
"anthropic-version": "2023-06-01",
|
|
1089
|
+
"X-Punk-App": this.app,
|
|
1090
|
+
"X-Punk-Agent": this.agent,
|
|
1091
|
+
"X-Punk-Subject": this.subject
|
|
1092
|
+
}),
|
|
1093
|
+
body: JSON.stringify({ ...params, stream: true })
|
|
1094
|
+
});
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
throw this.toTransportError(method, path, err);
|
|
1097
|
+
}
|
|
1098
|
+
if (!res.ok)
|
|
1099
|
+
throw this.toError(method, path, res, await res.text());
|
|
1100
|
+
const runId = res.headers.get("x-punk-run-id") ?? "";
|
|
1101
|
+
const route = res.headers.get("x-punk-route") ?? "unknown";
|
|
1102
|
+
let model;
|
|
1103
|
+
let usage;
|
|
1104
|
+
for await (const frame of sseFrames(res, method, path)) {
|
|
1105
|
+
const raw = parseSseJson(frame, method, path);
|
|
1106
|
+
if (typeof raw?.message?.model === "string")
|
|
1107
|
+
model = raw.message.model;
|
|
1108
|
+
const messageUsage = normalizeAnthropicUsage(raw.message);
|
|
1109
|
+
if (messageUsage)
|
|
1110
|
+
usage = messageUsage;
|
|
1111
|
+
if (isRecord(raw?.usage)) {
|
|
1112
|
+
usage = normalizeAnthropicUsage({ usage: { ...usage?.raw, ...raw.usage } });
|
|
1113
|
+
}
|
|
1114
|
+
if (raw?.type === "content_block_delta") {
|
|
1115
|
+
yield {
|
|
1116
|
+
type: "delta",
|
|
1117
|
+
content: typeof raw?.delta?.text === "string" ? raw.delta.text : "",
|
|
1118
|
+
runId,
|
|
1119
|
+
route,
|
|
1120
|
+
event: frame.event,
|
|
1121
|
+
raw,
|
|
1122
|
+
usage,
|
|
1123
|
+
model,
|
|
1124
|
+
provider: normalizeProvider(raw)
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
if (raw?.type === "message_stop") {
|
|
1128
|
+
yield {
|
|
1129
|
+
type: "done",
|
|
1130
|
+
content: "",
|
|
1131
|
+
runId,
|
|
1132
|
+
route,
|
|
1133
|
+
event: frame.event,
|
|
1134
|
+
raw,
|
|
1135
|
+
usage,
|
|
1136
|
+
model,
|
|
1137
|
+
provider: normalizeProvider(raw)
|
|
1138
|
+
};
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
yield { type: "done", content: "", runId, route, usage, model };
|
|
1143
|
+
}
|
|
725
1144
|
traceTool(def) {
|
|
726
1145
|
const toolName = requireCleanToolName(def.name);
|
|
727
1146
|
const level = classifySdkToolSideEffect(toolName, def.sideEffectLevel);
|
|
728
1147
|
const ttlSeconds = normalizeTtlSeconds(def.ttlSeconds);
|
|
729
1148
|
const schemaFp = cleanOptionalCacheDimension("schemaFp", def.schemaFp);
|
|
730
1149
|
return async (args, ctx) => {
|
|
731
|
-
const runId = cleanIdentityValue(ctx?.runId);
|
|
1150
|
+
const runId = cleanIdentityValue(ctx?.runId) ?? this.currentRunId();
|
|
732
1151
|
const cacheable = level <= 1 && ttlSeconds !== undefined && runId !== undefined && this.subject !== undefined;
|
|
733
1152
|
if (runId) {
|
|
734
1153
|
const decision = await this.tryTraceDecision(runId, "tool.called", {
|
|
@@ -906,6 +1325,51 @@ class Punk {
|
|
|
906
1325
|
const path = `/api/v1/runs/${encodeURIComponent(cleanPathId("run id", id))}`;
|
|
907
1326
|
return requireRunDetailResult(await this.request("GET", path), "GET", path);
|
|
908
1327
|
}
|
|
1328
|
+
async explain(runId) {
|
|
1329
|
+
return this.runDetail(runId).then((detail) => detail.run.routeExplanation ?? null);
|
|
1330
|
+
}
|
|
1331
|
+
async savingsForRun(runId) {
|
|
1332
|
+
const detail = await this.runDetail(runId);
|
|
1333
|
+
return {
|
|
1334
|
+
runId: detail.run.id,
|
|
1335
|
+
route: detail.run.route,
|
|
1336
|
+
status: detail.run.status,
|
|
1337
|
+
costUsd: detail.run.costUsd,
|
|
1338
|
+
savedUsd: detail.run.savedUsd,
|
|
1339
|
+
ghostSavedUsd: detail.run.ghostSavedUsd ?? 0,
|
|
1340
|
+
latencyMs: detail.run.latencyMs,
|
|
1341
|
+
inputTokens: detail.run.inputTokens,
|
|
1342
|
+
outputTokens: detail.run.outputTokens
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
async sideEffectsForRun(runId) {
|
|
1346
|
+
return this.runDetail(runId).then((detail) => detail.sideEffects);
|
|
1347
|
+
}
|
|
1348
|
+
async waitForRun(runId, opts = {}) {
|
|
1349
|
+
for await (const detail of this.watchRun(runId, opts)) {
|
|
1350
|
+
if (isTerminalRunStatus(detail.run.status))
|
|
1351
|
+
return detail;
|
|
1352
|
+
}
|
|
1353
|
+
throw new Error(`Punk waitForRun(${cleanPathId("run id", runId)}) ended before the run reached a terminal status`);
|
|
1354
|
+
}
|
|
1355
|
+
async* watchRun(runId, opts = {}) {
|
|
1356
|
+
const id = cleanPathId("run id", runId);
|
|
1357
|
+
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_WAIT_POLL_INTERVAL_MS;
|
|
1358
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1359
|
+
const startedAt = Date.now();
|
|
1360
|
+
while (true) {
|
|
1361
|
+
if (opts.signal?.aborted)
|
|
1362
|
+
throw new Error("Punk watchRun aborted");
|
|
1363
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
1364
|
+
throw new Error(`Punk watchRun(${id}) timed out after ${timeoutMs}ms`);
|
|
1365
|
+
}
|
|
1366
|
+
const detail = await this.runDetail(id);
|
|
1367
|
+
yield detail;
|
|
1368
|
+
if (isTerminalRunStatus(detail.run.status))
|
|
1369
|
+
return;
|
|
1370
|
+
await checkedDelay(pollIntervalMs, opts.signal);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
909
1373
|
async receipt(id) {
|
|
910
1374
|
const path = `/api/v1/receipts/${encodeURIComponent(cleanPathId("receipt id", id))}`;
|
|
911
1375
|
return requireReceiptResult(await this.request("GET", path), "GET", path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@punktechnologies/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Gateway Agnostic AI runtime SDK for OpenAI, Anthropic, OpenRouter, and more, with agent tracing, tool caching, governance, observability, and cost optimization.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|