coze_lab 0.1.42 → 0.1.44
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
CHANGED
|
@@ -69,6 +69,9 @@ created after a new Codex turn, Codex did not load or execute the hook.
|
|
|
69
69
|
## Token lifecycle
|
|
70
70
|
|
|
71
71
|
OAuth tokens are stored in `~/.cozeloop/credentials.json` (mode 600).
|
|
72
|
+
For local `--agent-id=<agentId>` setup, if
|
|
73
|
+
`~/.coze/agents/<agentId>/config.json` contains `patToken`, onboarding uses that
|
|
74
|
+
PAT directly and skips the OAuth device-code flow.
|
|
72
75
|
|
|
73
76
|
In cloud mode, trace verification and hook uploads prefer
|
|
74
77
|
`COZELOOP_API_TOKEN` and fall back to `COZE_API_TOKEN`. The selfcheck result is
|
|
@@ -77,7 +80,8 @@ writes the hook configuration but reports `verify=fail` with `token_source`.
|
|
|
77
80
|
For Python SDK uploads, `OTEL_ENDPOINT` is not used as the SDK base URL; set
|
|
78
81
|
`COZELOOP_API_BASE_URL` only when the SDK ingest endpoint should be overridden.
|
|
79
82
|
|
|
80
|
-
**At hook execution time** (Claude Code / Codex),
|
|
83
|
+
**At hook execution time** (Claude Code / Codex), per-agent PAT setups use the
|
|
84
|
+
token written by onboarding. Otherwise, the Python hook script automatically:
|
|
81
85
|
1. Reads `~/.cozeloop/credentials.json`
|
|
82
86
|
2. If the token expires in < 10 minutes, calls the Coze refresh API
|
|
83
87
|
3. Updates `credentials.json` with the new token
|
package/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const CREDS_PATH = require('path').join(require('os').homedir(), '.cozeloop', '
|
|
|
9
9
|
const PACKAGE_VERSION = require('./package.json').version;
|
|
10
10
|
// Refresh when less than 10 minutes remain
|
|
11
11
|
const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
|
|
12
|
+
const TOKEN_SOURCE_AGENT_PAT = 'agent_config.patToken';
|
|
12
13
|
|
|
13
14
|
// ─── 1. Cloud structured output ──────────────────────────────────────────────
|
|
14
15
|
// 云端模式:在 stdout 输出一行机器可读结果 COZE_LAB_RESULT={...},
|
|
@@ -176,6 +177,10 @@ function hasCloudModelInfo(cfg) {
|
|
|
176
177
|
return !!(cfg?.modelInfo && typeof cfg.modelInfo === 'object' && !Array.isArray(cfg.modelInfo));
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
function getAgentPatToken(cfg) {
|
|
181
|
+
return typeof cfg?.patToken === 'string' ? cfg.patToken.trim() : '';
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
function inferDeployTypeFromAgentConfig(cfg) {
|
|
180
185
|
if (cfg?.deployType === 'cloud') return { deployType: 'cloud', reason: 'config.deployType=cloud' };
|
|
181
186
|
if (isCloudRuntimeEnv()) return { deployType: 'cloud', reason: 'env CLOUD_ENV=1' };
|
|
@@ -216,7 +221,7 @@ function resolveAgent(agentId, soft) {
|
|
|
216
221
|
]);
|
|
217
222
|
}
|
|
218
223
|
const inferred = inferDeployTypeFromAgentConfig(cfg);
|
|
219
|
-
return { framework, workspace: cfg.workspace || '', deployType: inferred.deployType, deployReason: inferred.reason, agentId, root };
|
|
224
|
+
return { framework, workspace: cfg.workspace || '', deployType: inferred.deployType, deployReason: inferred.reason, agentId, root, patToken: getAgentPatToken(cfg) };
|
|
220
225
|
}
|
|
221
226
|
|
|
222
227
|
function validateArgs(args) {
|
|
@@ -239,6 +244,7 @@ function validateArgs(args) {
|
|
|
239
244
|
agentId: resolved.agentId,
|
|
240
245
|
workspace: resolved.workspace,
|
|
241
246
|
agentRoot: resolved.root,
|
|
247
|
+
patToken: resolved.patToken,
|
|
242
248
|
deployType: resolved.deployType,
|
|
243
249
|
deployReason: explicitCloud ? '--cloud' : resolved.deployReason,
|
|
244
250
|
'codex-home': args['codex-home'],
|
|
@@ -527,7 +533,7 @@ function shellEnvLine(key, value) {
|
|
|
527
533
|
// writeClaudeCodeHook 配置 Claude Code 的 hook。
|
|
528
534
|
// configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
|
|
529
535
|
// settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
|
|
530
|
-
function writeClaudeCodeHook(
|
|
536
|
+
function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud, tokenSource) {
|
|
531
537
|
const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
|
|
532
538
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
533
539
|
const baseDir = configBaseDir || process.cwd();
|
|
@@ -610,9 +616,16 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
610
616
|
existing.env.COZE_API_TOKEN = cozeToken;
|
|
611
617
|
delete existing.env.COZELOOP_API_TOKEN;
|
|
612
618
|
}
|
|
619
|
+
delete existing.env.COZELOOP_TOKEN_SOURCE;
|
|
620
|
+
} else if (tokenSource === TOKEN_SOURCE_AGENT_PAT && token) {
|
|
621
|
+
existing.env.COZELOOP_API_TOKEN = token;
|
|
622
|
+
existing.env.COZELOOP_TOKEN_SOURCE = TOKEN_SOURCE_AGENT_PAT;
|
|
623
|
+
delete existing.env.COZE_API_TOKEN;
|
|
624
|
+
delete existing.env.COZELAB_ONBOARD_CLOUD;
|
|
613
625
|
} else {
|
|
614
626
|
delete existing.env.COZELOOP_API_TOKEN;
|
|
615
627
|
delete existing.env.COZE_API_TOKEN;
|
|
628
|
+
delete existing.env.COZELOOP_TOKEN_SOURCE;
|
|
616
629
|
delete existing.env.COZELAB_ONBOARD_CLOUD;
|
|
617
630
|
}
|
|
618
631
|
const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
|
|
@@ -645,9 +658,10 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
645
658
|
|
|
646
659
|
// writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
|
|
647
660
|
// 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
|
|
648
|
-
//
|
|
661
|
+
// 普通本地模式不把短期 token 写入 cozeloop.env;Hook 运行时读取 ~/.cozeloop/credentials.json。
|
|
662
|
+
// 本地 --agent-id 且 config.patToken 存在时写入该 PAT,并用 COZELOOP_TOKEN_SOURCE 标记来源。
|
|
649
663
|
// cloud=true 时写 COZELAB_ONBOARD_CLOUD,并带入 sandbox 注入的 trace token。
|
|
650
|
-
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
664
|
+
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSource) {
|
|
651
665
|
const home = codexHome || path.join(os.homedir(), '.codex');
|
|
652
666
|
const hooksDir = path.join(home, 'hooks');
|
|
653
667
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
@@ -675,6 +689,9 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
|
675
689
|
} else if (cozeToken) {
|
|
676
690
|
envLines.push(shellEnvLine('COZE_API_TOKEN', cozeToken));
|
|
677
691
|
}
|
|
692
|
+
} else if (tokenSource === TOKEN_SOURCE_AGENT_PAT && token) {
|
|
693
|
+
envLines.push(shellEnvLine('COZELOOP_API_TOKEN', token));
|
|
694
|
+
envLines.push(shellEnvLine('COZELOOP_TOKEN_SOURCE', TOKEN_SOURCE_AGENT_PAT));
|
|
678
695
|
}
|
|
679
696
|
envLines.push(shellEnvLine('CODEX_HOME', home));
|
|
680
697
|
envLines.push(shellEnvLine('COZELOOP_HOOK_LOG', logFile));
|
|
@@ -879,7 +896,7 @@ function buildOpenClawSelfcheckTags(pcfg, workspaceId, pair) {
|
|
|
879
896
|
return tags;
|
|
880
897
|
}
|
|
881
898
|
|
|
882
|
-
function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile) {
|
|
899
|
+
function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile, tokenSource) {
|
|
883
900
|
if (!existing.plugins) existing.plugins = {};
|
|
884
901
|
if (!existing.plugins.allow) existing.plugins.allow = [];
|
|
885
902
|
if (!existing.plugins.entries) existing.plugins.entries = {};
|
|
@@ -906,7 +923,12 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud,
|
|
|
906
923
|
if (logFile) {
|
|
907
924
|
pcfg.logFile = logFile;
|
|
908
925
|
}
|
|
909
|
-
|
|
926
|
+
if (tokenSource === TOKEN_SOURCE_AGENT_PAT) {
|
|
927
|
+
pcfg.tokenSource = TOKEN_SOURCE_AGENT_PAT;
|
|
928
|
+
} else {
|
|
929
|
+
delete pcfg.tokenSource;
|
|
930
|
+
}
|
|
931
|
+
pcfg.disableLocalCredentials = !!cloud || tokenSource === TOKEN_SOURCE_AGENT_PAT;
|
|
910
932
|
// 插件代码版本:参与幂等比对,升级插件(bump scripts/openclaw/package.json version)后
|
|
911
933
|
// 强制触发重写+重装+重启,避免云端 pluginDir 滞留旧插件 dist。
|
|
912
934
|
if (OPENCLAW_PLUGIN_VERSION) {
|
|
@@ -924,7 +946,7 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud,
|
|
|
924
946
|
return existing;
|
|
925
947
|
}
|
|
926
948
|
|
|
927
|
-
function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile) {
|
|
949
|
+
function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile, tokenSource) {
|
|
928
950
|
if (!fs.existsSync(pluginDir)) return false;
|
|
929
951
|
let existing;
|
|
930
952
|
try {
|
|
@@ -939,11 +961,12 @@ function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, ag
|
|
|
939
961
|
agentId,
|
|
940
962
|
cloud,
|
|
941
963
|
logFile,
|
|
964
|
+
tokenSource,
|
|
942
965
|
);
|
|
943
966
|
return JSON.stringify(existing) === JSON.stringify(desired);
|
|
944
967
|
}
|
|
945
968
|
|
|
946
|
-
function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
|
|
969
|
+
function writeOpenClawHook(token, workspaceId, agentId, cloud, force, tokenSource) {
|
|
947
970
|
const home = resolveHomeDir(cloud);
|
|
948
971
|
const configPath = path.join(home, '.openclaw', 'openclaw.json');
|
|
949
972
|
const pluginDir = path.join(home, '.cozeloop', 'openclaw-plugin');
|
|
@@ -958,7 +981,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
|
|
|
958
981
|
]);
|
|
959
982
|
}
|
|
960
983
|
|
|
961
|
-
if (!force && isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile)) {
|
|
984
|
+
if (!force && isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile, tokenSource)) {
|
|
962
985
|
ok(`OpenClaw plugin already configured in ${configPath}`);
|
|
963
986
|
info('OpenClaw gateway restart skipped (configuration unchanged).');
|
|
964
987
|
return { configPath, pluginDir, logFile, unchanged: true, gatewayRestarted: false, gatewayRestartSkipped: true };
|
|
@@ -1017,7 +1040,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
|
|
|
1017
1040
|
|
|
1018
1041
|
// 4. Update openclaw.json with token and workspace
|
|
1019
1042
|
const config = mergeJson(configPath, (existing) => {
|
|
1020
|
-
return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile);
|
|
1043
|
+
return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile, tokenSource);
|
|
1021
1044
|
});
|
|
1022
1045
|
|
|
1023
1046
|
try {
|
|
@@ -1886,7 +1909,8 @@ async function main() {
|
|
|
1886
1909
|
// Step 1: Authorize.
|
|
1887
1910
|
// 云端模式:token 取自 sandbox 注入的环境变量,跳过 OAuth / credentials.json。
|
|
1888
1911
|
// 优先使用 COZELOOP_API_TOKEN;兼容使用 COZE_API_TOKEN,并以真实 selfcheck 为准。
|
|
1889
|
-
//
|
|
1912
|
+
// 本地 --agent-id 且 config.patToken 存在:优先使用该 PAT,跳过 OAuth / credentials.json。
|
|
1913
|
+
// 其它本地模式:load cached → refresh → device code。
|
|
1890
1914
|
// 注意:workspace_id 始终用写死的 WORKSPACE_ID(团队固定上报 workspace),不读环境。
|
|
1891
1915
|
let token;
|
|
1892
1916
|
let tokenSource = '';
|
|
@@ -1910,8 +1934,14 @@ async function main() {
|
|
|
1910
1934
|
}
|
|
1911
1935
|
} else {
|
|
1912
1936
|
info('Step 1/5: 检查授权状态...');
|
|
1913
|
-
|
|
1914
|
-
|
|
1937
|
+
if (args.agentId && args.patToken) {
|
|
1938
|
+
token = args.patToken;
|
|
1939
|
+
tokenSource = TOKEN_SOURCE_AGENT_PAT;
|
|
1940
|
+
ok(`已从 ~/.coze/agents/${args.agentId}/config.json 读取 patToken,跳过本地授权。`);
|
|
1941
|
+
} else {
|
|
1942
|
+
token = await getValidToken();
|
|
1943
|
+
tokenSource = 'credentials';
|
|
1944
|
+
}
|
|
1915
1945
|
}
|
|
1916
1946
|
console.log('');
|
|
1917
1947
|
|
|
@@ -1945,7 +1975,7 @@ async function main() {
|
|
|
1945
1975
|
'否则 hook 配置会落到全局/进程目录,无法做到 per-agent 隔离。',
|
|
1946
1976
|
]);
|
|
1947
1977
|
}
|
|
1948
|
-
written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud);
|
|
1978
|
+
written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud, tokenSource);
|
|
1949
1979
|
} else if (agent === 'codex') {
|
|
1950
1980
|
const codexHome = resolveCodexHome(args);
|
|
1951
1981
|
if (args.cloud && args.agentId && codexHome && !fs.existsSync(codexHome)) {
|
|
@@ -1953,10 +1983,10 @@ async function main() {
|
|
|
1953
1983
|
info(`已创建云端 Codex 配置目录: ${codexHome}`);
|
|
1954
1984
|
}
|
|
1955
1985
|
if (codexHome) info(`Codex 配置目录: ${codexHome}`);
|
|
1956
|
-
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud);
|
|
1986
|
+
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud, tokenSource);
|
|
1957
1987
|
} else {
|
|
1958
1988
|
// openclaw:云端用 traceAgentIds allowlist 做 per-agent 放行。
|
|
1959
|
-
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud, args.force) || {};
|
|
1989
|
+
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud, args.force, tokenSource) || {};
|
|
1960
1990
|
}
|
|
1961
1991
|
// 走到这里说明 detectAgent / 环境检查 / 写 hook 配置全部成功 → 注入成功。
|
|
1962
1992
|
cloudResult.inject = 'ok';
|
|
@@ -2056,6 +2086,7 @@ if (require.main === module) {
|
|
|
2056
2086
|
|
|
2057
2087
|
module.exports = {
|
|
2058
2088
|
resolveHomeDir,
|
|
2089
|
+
getAgentPatToken,
|
|
2059
2090
|
mergeJson,
|
|
2060
2091
|
atomicWriteFileSync,
|
|
2061
2092
|
isExpired,
|
package/package.json
CHANGED
|
@@ -369,6 +369,9 @@ def force_refresh_token_after_upload_failure(reason: str = "upload_failure", cur
|
|
|
369
369
|
if is_cloud:
|
|
370
370
|
hook_log("upload failure token refresh skipped in cloud mode")
|
|
371
371
|
return None
|
|
372
|
+
if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken":
|
|
373
|
+
hook_log("upload failure token refresh skipped for agent config patToken")
|
|
374
|
+
return None
|
|
372
375
|
|
|
373
376
|
creds = _load_credentials()
|
|
374
377
|
if not creds or not creds.get("refresh_token"):
|
|
@@ -461,6 +464,8 @@ def get_fresh_token() -> Optional[str]:
|
|
|
461
464
|
env_token = os.environ.get("COZELOOP_API_TOKEN")
|
|
462
465
|
env_coze_token = os.environ.get("COZE_API_TOKEN")
|
|
463
466
|
is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
|
|
467
|
+
if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken" and env_token:
|
|
468
|
+
return env_token
|
|
464
469
|
if is_cloud:
|
|
465
470
|
return env_token or env_coze_token or _token_from_credentials()
|
|
466
471
|
|
|
@@ -550,6 +555,19 @@ def read_new_messages(file_path: str, start_line: int = 0) -> List[Dict[str, Any
|
|
|
550
555
|
|
|
551
556
|
# --- Content Helpers ---
|
|
552
557
|
|
|
558
|
+
def _usage_int(usage: Any, key: str) -> int:
|
|
559
|
+
"""Read a token count from a usage dict, treating missing/None/非数字 一律为 0。
|
|
560
|
+
|
|
561
|
+
Claude Code transcript 里 usage 的 cache_* 等字段常【存在但值为 null】,dict.get(key, 0)
|
|
562
|
+
对显式 null 返回 None 而非 0,后续 None + int / None > 0 会抛 TypeError,导致整个实时
|
|
563
|
+
上报失败、trace 查不到。这里统一兜底。
|
|
564
|
+
"""
|
|
565
|
+
if not isinstance(usage, dict):
|
|
566
|
+
return 0
|
|
567
|
+
v = usage.get(key)
|
|
568
|
+
return v if isinstance(v, int) else 0
|
|
569
|
+
|
|
570
|
+
|
|
553
571
|
def is_empty_content(content: Any) -> bool:
|
|
554
572
|
"""Return True if content carries no meaningful data."""
|
|
555
573
|
if content is None:
|
|
@@ -676,7 +694,7 @@ def _group_subagent_steps(progress_msgs: List[Dict[str, Any]]) -> List[Dict[str,
|
|
|
676
694
|
existing.extend(content)
|
|
677
695
|
last_step["tool_calls"].extend(tool_calls)
|
|
678
696
|
usage = pmsg.get("usage", {})
|
|
679
|
-
if usage
|
|
697
|
+
if _usage_int(usage, "input_tokens") > 0 or _usage_int(usage, "output_tokens") > 0:
|
|
680
698
|
last_step["assistant_message"]["message"]["usage"] = usage
|
|
681
699
|
else:
|
|
682
700
|
steps.append({
|
|
@@ -849,7 +867,7 @@ def group_messages_into_turns(messages: List[Dict[str, Any]]) -> List[Dict[str,
|
|
|
849
867
|
last_step["tool_calls"].extend(tool_calls)
|
|
850
868
|
# Carry over usage from the later line (earlier line typically has zeros)
|
|
851
869
|
usage = message.get("usage", {})
|
|
852
|
-
if usage
|
|
870
|
+
if _usage_int(usage, "input_tokens") > 0 or _usage_int(usage, "output_tokens") > 0:
|
|
853
871
|
last_step["assistant_message"]["message"]["usage"] = usage
|
|
854
872
|
else:
|
|
855
873
|
# New API response — create a new step
|
|
@@ -1329,10 +1347,10 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1329
1347
|
|
|
1330
1348
|
# Set token usage for this specific model call
|
|
1331
1349
|
usage = assistant_message_obj.get("usage", {})
|
|
1332
|
-
input_tokens = usage
|
|
1333
|
-
output_tokens = usage
|
|
1334
|
-
cache_creation = usage
|
|
1335
|
-
cache_read = usage
|
|
1350
|
+
input_tokens = _usage_int(usage, "input_tokens")
|
|
1351
|
+
output_tokens = _usage_int(usage, "output_tokens")
|
|
1352
|
+
cache_creation = _usage_int(usage, "cache_creation_input_tokens")
|
|
1353
|
+
cache_read = _usage_int(usage, "cache_read_input_tokens")
|
|
1336
1354
|
if input_tokens > 0 or cache_creation > 0 or cache_read > 0:
|
|
1337
1355
|
model_span.set_input_tokens(input_tokens + cache_creation + cache_read)
|
|
1338
1356
|
if output_tokens > 0:
|
|
@@ -1402,10 +1420,10 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1402
1420
|
|
|
1403
1421
|
# Distribute total usage evenly across sub-agent model steps.
|
|
1404
1422
|
total_usage = tool_call.get("_total_usage", {})
|
|
1405
|
-
total_in = (total_usage
|
|
1406
|
-
+ total_usage
|
|
1407
|
-
+ total_usage
|
|
1408
|
-
total_out = total_usage
|
|
1423
|
+
total_in = (_usage_int(total_usage, "input_tokens")
|
|
1424
|
+
+ _usage_int(total_usage, "cache_creation_input_tokens")
|
|
1425
|
+
+ _usage_int(total_usage, "cache_read_input_tokens"))
|
|
1426
|
+
total_out = _usage_int(total_usage, "output_tokens")
|
|
1409
1427
|
n_model_steps = len(sub_steps)
|
|
1410
1428
|
per_step_in = total_in // n_model_steps if n_model_steps > 0 else 0
|
|
1411
1429
|
per_step_out = total_out // n_model_steps if n_model_steps > 0 else 0
|
|
@@ -1778,12 +1796,12 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
|
|
|
1778
1796
|
if text_parts:
|
|
1779
1797
|
mspan.set_output("\n".join(text_parts))
|
|
1780
1798
|
usage = amo.get("usage", {})
|
|
1781
|
-
it_tok = usage
|
|
1799
|
+
it_tok = _usage_int(usage, "input_tokens") + _usage_int(usage, "cache_creation_input_tokens") + _usage_int(usage, "cache_read_input_tokens")
|
|
1782
1800
|
if it_tok > 0:
|
|
1783
1801
|
try: mspan.set_input_tokens(it_tok)
|
|
1784
1802
|
except Exception: pass
|
|
1785
|
-
if usage
|
|
1786
|
-
try: mspan.set_output_tokens(usage
|
|
1803
|
+
if _usage_int(usage, "output_tokens") > 0:
|
|
1804
|
+
try: mspan.set_output_tokens(_usage_int(usage, "output_tokens"))
|
|
1787
1805
|
except Exception: pass
|
|
1788
1806
|
mspan_ctx = client.get_span_from_header(mspan.to_header())
|
|
1789
1807
|
mspan.finish()
|
|
@@ -279,6 +279,9 @@ def force_refresh_token_after_upload_failure(reason: str = "upload_failure", cur
|
|
|
279
279
|
if is_cloud:
|
|
280
280
|
hook_log("upload failure token refresh skipped in cloud mode")
|
|
281
281
|
return None
|
|
282
|
+
if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken":
|
|
283
|
+
hook_log("upload failure token refresh skipped for agent config patToken")
|
|
284
|
+
return None
|
|
282
285
|
|
|
283
286
|
creds = _load_credentials()
|
|
284
287
|
if not creds or not creds.get("refresh_token"):
|
|
@@ -370,6 +373,8 @@ def get_fresh_token():
|
|
|
370
373
|
env_token = os.environ.get("COZELOOP_API_TOKEN")
|
|
371
374
|
env_coze_token = os.environ.get("COZE_API_TOKEN")
|
|
372
375
|
is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
|
|
376
|
+
if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken" and env_token:
|
|
377
|
+
return env_token
|
|
373
378
|
if is_cloud:
|
|
374
379
|
return env_token or env_coze_token or _token_from_credentials()
|
|
375
380
|
|
|
@@ -787,6 +792,18 @@ def truncate_text(text: str, limit: int = 12000) -> str:
|
|
|
787
792
|
|
|
788
793
|
# --- Message Grouping ---
|
|
789
794
|
|
|
795
|
+
def _usage_int(usage, key):
|
|
796
|
+
"""从 token_usage dict 读 token 数,missing/None/非数字 一律按 0。
|
|
797
|
+
|
|
798
|
+
transcript 里 token_usage 字段可能【存在但值为 null】,dict.get(key, 0) 对显式 null
|
|
799
|
+
返回 None 而非 0,后续 None > 0 / None + int 会抛 TypeError 中断上报。这里统一兜底。
|
|
800
|
+
"""
|
|
801
|
+
if not isinstance(usage, dict):
|
|
802
|
+
return 0
|
|
803
|
+
v = usage.get(key)
|
|
804
|
+
return v if isinstance(v, int) else 0
|
|
805
|
+
|
|
806
|
+
|
|
790
807
|
def _parse_ts(obj):
|
|
791
808
|
"""从 codex entry/payload 的 timestamp 解析 datetime(带时区)。失败返回 None。
|
|
792
809
|
|
|
@@ -1299,8 +1316,8 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1299
1316
|
|
|
1300
1317
|
# Set token usage
|
|
1301
1318
|
token_usage = turn.get("token_usage", {})
|
|
1302
|
-
input_tokens = token_usage
|
|
1303
|
-
output_tokens = token_usage
|
|
1319
|
+
input_tokens = _usage_int(token_usage, "input_tokens")
|
|
1320
|
+
output_tokens = _usage_int(token_usage, "output_tokens")
|
|
1304
1321
|
if input_tokens > 0:
|
|
1305
1322
|
model_span.set_input_tokens(input_tokens)
|
|
1306
1323
|
if output_tokens > 0:
|
|
@@ -1446,10 +1463,12 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1446
1463
|
sa_model_span.set_output(ModelOutput(choices=sa_choices))
|
|
1447
1464
|
|
|
1448
1465
|
sa_token = sa_turn.get("token_usage", {})
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if
|
|
1452
|
-
sa_model_span.
|
|
1466
|
+
sa_in = _usage_int(sa_token, "input_tokens")
|
|
1467
|
+
sa_out = _usage_int(sa_token, "output_tokens")
|
|
1468
|
+
if sa_in > 0:
|
|
1469
|
+
sa_model_span.set_input_tokens(sa_in)
|
|
1470
|
+
if sa_out > 0:
|
|
1471
|
+
sa_model_span.set_output_tokens(sa_out)
|
|
1453
1472
|
|
|
1454
1473
|
# Subagent tool spans
|
|
1455
1474
|
for sa_tc in sa_turn.get("tool_calls", []):
|
|
@@ -187,6 +187,7 @@ async function _forceRefreshToken(currentAuthorization, opts = {}) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
async function getRefreshedToken(currentAuthorization, opts = {}) {
|
|
190
|
+
if (opts.tokenSource === "agent_config.patToken") return currentAuthorization;
|
|
190
191
|
if (opts.disableLocalCredentials) return currentAuthorization;
|
|
191
192
|
if (opts.force) return _forceRefreshToken(currentAuthorization, opts);
|
|
192
193
|
const creds = _loadCreds();
|
|
@@ -565,6 +566,7 @@ export class CozeloopExporter {
|
|
|
565
566
|
async refreshAuthIfNeeded() {
|
|
566
567
|
const fresh = await getRefreshedToken(this.config.authorization, {
|
|
567
568
|
disableLocalCredentials: this.config.disableLocalCredentials,
|
|
569
|
+
tokenSource: this.config.tokenSource,
|
|
568
570
|
});
|
|
569
571
|
if (fresh && fresh !== this.config.authorization) {
|
|
570
572
|
this.api.logger.info("[CozeloopTrace] Token refreshed, re-initializing exporter...");
|
|
@@ -585,6 +587,7 @@ export class CozeloopExporter {
|
|
|
585
587
|
async refreshAuthAfterUploadFailure(currentAuthorization, err) {
|
|
586
588
|
const fresh = await getRefreshedToken(currentAuthorization || this.config.authorization, {
|
|
587
589
|
disableLocalCredentials: this.config.disableLocalCredentials,
|
|
590
|
+
tokenSource: this.config.tokenSource,
|
|
588
591
|
force: true,
|
|
589
592
|
reason: err?.message || "upload_failure",
|
|
590
593
|
logFile: this.config.logFile,
|
|
@@ -540,6 +540,7 @@ const cozeloopTracePlugin = {
|
|
|
540
540
|
batchInterval: pluginConfig.batchInterval || 500,
|
|
541
541
|
enabledHooks: pluginConfig.enabledHooks,
|
|
542
542
|
disableLocalCredentials: pluginConfig.disableLocalCredentials === true,
|
|
543
|
+
tokenSource: pluginConfig.tokenSource,
|
|
543
544
|
logFile: pluginConfig.logFile,
|
|
544
545
|
};
|
|
545
546
|
const exporter = new CozeloopExporter(api, config);
|