@x-otto/plugin-anthropic 0.1.0-alpha.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 +39 -0
- package/otto-plugin.json +49 -0
- package/package.json +28 -0
- package/plugin-dist/meta.json +1 -0
- package/plugin-dist/plugin.cjs +52320 -0
- package/plugin-dist/plugin.js +104 -0
- package/plugin.ts +51 -0
- package/src/anthropic-oauth-client-identity.ts +101 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const __otto_plugin_import_meta_url = require("url").pathToFileURL(__filename).href;
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// extensions/plugin-anthropic/plugin.ts
|
|
22
|
+
var plugin_exports = {};
|
|
23
|
+
__export(plugin_exports, {
|
|
24
|
+
default: () => plugin_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(plugin_exports);
|
|
27
|
+
|
|
28
|
+
// otto-sdk-shim:otto-plugin-sdk
|
|
29
|
+
var definePlugin = (f) => f;
|
|
30
|
+
|
|
31
|
+
// extensions/plugin-anthropic/src/claude-code-spoof.ts
|
|
32
|
+
var import_node_crypto = require("node:crypto");
|
|
33
|
+
var CLAUDE_CODE_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
34
|
+
var CLAUDE_CODE_BETA = "claude-code-20250219";
|
|
35
|
+
var CLAUDE_CODE_VERSION = process.env["OTTO_CLAUDE_CODE_VERSION"] || "2.0.1";
|
|
36
|
+
function mergeClaudeCodeBeta(beta) {
|
|
37
|
+
const parts = (beta ? beta.split(",") : []).map((s) => s.trim()).filter(Boolean);
|
|
38
|
+
if (!parts.includes(CLAUDE_CODE_BETA)) {
|
|
39
|
+
parts.push(CLAUDE_CODE_BETA);
|
|
40
|
+
}
|
|
41
|
+
return parts.join(",");
|
|
42
|
+
}
|
|
43
|
+
function claudeCodeOAuthHeaders(beta, sessionId) {
|
|
44
|
+
return {
|
|
45
|
+
"anthropic-beta": mergeClaudeCodeBeta(beta),
|
|
46
|
+
"x-app": "cli",
|
|
47
|
+
"User-Agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
|
|
48
|
+
"X-Claude-Code-Session-Id": sessionId
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
var cachedDeviceId;
|
|
52
|
+
function deviceId() {
|
|
53
|
+
if (!cachedDeviceId) {
|
|
54
|
+
cachedDeviceId = process.env["OTTO_CLAUDE_CODE_DEVICE_ID"] || (0, import_node_crypto.randomUUID)();
|
|
55
|
+
}
|
|
56
|
+
return cachedDeviceId;
|
|
57
|
+
}
|
|
58
|
+
function claudeCodeMetadata(sessionId, accountUuid = "") {
|
|
59
|
+
return {
|
|
60
|
+
user_id: JSON.stringify({
|
|
61
|
+
device_id: deviceId(),
|
|
62
|
+
account_uuid: accountUuid,
|
|
63
|
+
session_id: sessionId
|
|
64
|
+
})
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function prependClaudeCodeIdentity(blocks) {
|
|
68
|
+
const prefix = { type: "text", text: CLAUDE_CODE_SYSTEM_PREFIX };
|
|
69
|
+
if (!blocks || blocks.length === 0) {
|
|
70
|
+
return [prefix];
|
|
71
|
+
}
|
|
72
|
+
if (blocks[0]?.text === CLAUDE_CODE_SYSTEM_PREFIX) {
|
|
73
|
+
return blocks;
|
|
74
|
+
}
|
|
75
|
+
return [prefix, ...blocks];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// extensions/plugin-anthropic/plugin.ts
|
|
79
|
+
function customizeRequest(input) {
|
|
80
|
+
if (input.auth.mode !== "oauth") return {};
|
|
81
|
+
return {
|
|
82
|
+
defaultHeaders: claudeCodeOAuthHeaders(input.auth.beta, input.sessionId),
|
|
83
|
+
transformSystemBlocks: (blocks) => prependClaudeCodeIdentity(blocks),
|
|
84
|
+
metadata: claudeCodeMetadata(input.sessionId)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var plugin_default = definePlugin((ctx) => ({
|
|
88
|
+
providerFactories: {
|
|
89
|
+
anthropic: () => {
|
|
90
|
+
const protocol = ctx.getWireProtocol("anthropic-messages");
|
|
91
|
+
if (!protocol) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"wire protocol 'anthropic-messages' is not registered \u2014 is extensions/plugin-otto-wire-protocols installed and trusted? (RFC-148)"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return protocol.create({
|
|
97
|
+
resolveAuth: () => ctx.resolveProviderAuth("anthropic").then((a) => a ?? void 0),
|
|
98
|
+
resolveBaseUrl: async () => "https://api.anthropic.com",
|
|
99
|
+
headers: { "anthropic-version": "2023-06-01" },
|
|
100
|
+
customizeRequest
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}));
|
package/plugin.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugin.ts —— plugin-anthropic 代码式插件入口(RFC-148 M3)。
|
|
3
|
+
*
|
|
4
|
+
* 单轴化(评审 F-C1 CRITICAL 吸收):本插件从"声明式 contributes.providers + 框架层
|
|
5
|
+
* preserveProviderId 特判注入伪装"改为纯代码式——`otto-plugin.json` 的 `contributes.providers`
|
|
6
|
+
* 条目已删除(同一 api id 'anthropic' 双轴并存会因 source 不同被 first-win 拒绝其一,
|
|
7
|
+
* 详见 RFC-148 §D5),`contributes.oauth` 声明式部分保留不变,5 个静态模型迁至
|
|
8
|
+
* `codeProviderModels`(新 manifest 字段,RFC-148 M3 新增,见 packages/plugin/src/manifest.ts)。
|
|
9
|
+
*
|
|
10
|
+
* 官方 CLI 客户端伪装(绕 Anthropic 订阅 OAuth token 客户端分类器限流)现在是本插件
|
|
11
|
+
* 自己的 customizeRequest 定制器(复制 `extensions/plugin-github-copilot/plugin.ts` 的
|
|
12
|
+
* "代码式插件复用宿主注册的协议实现"模式,经 `ctx.getWireProtocol` 查表拿到
|
|
13
|
+
* `otto-wire-protocols` 插件提供的 anthropic-messages 协议实现)——框架层
|
|
14
|
+
* (provider-loader.ts)不再有任何 `preserveProviderId → 构造伪装 customizeRequest` 的
|
|
15
|
+
* 特判分支,彻底不认识具体官方客户端这个身份概念。
|
|
16
|
+
*/
|
|
17
|
+
import { definePlugin } from '@x-otto/plugin'
|
|
18
|
+
import {
|
|
19
|
+
anthropicOAuthClientHeaders,
|
|
20
|
+
anthropicOAuthRequestMetadata,
|
|
21
|
+
prependAnthropicOAuthIdentity,
|
|
22
|
+
} from './src/anthropic-oauth-client-identity'
|
|
23
|
+
import type { WireRequestCustomizeInput, WireRequestCustomization } from '@x-otto/provider'
|
|
24
|
+
|
|
25
|
+
function customizeRequest(input: WireRequestCustomizeInput): WireRequestCustomization {
|
|
26
|
+
if (input.auth.mode !== 'oauth') return {}
|
|
27
|
+
return {
|
|
28
|
+
defaultHeaders: anthropicOAuthClientHeaders(input.auth.beta, input.sessionId),
|
|
29
|
+
transformSystemBlocks: (blocks) => prependAnthropicOAuthIdentity(blocks),
|
|
30
|
+
metadata: anthropicOAuthRequestMetadata(input.sessionId),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default definePlugin((ctx) => ({
|
|
35
|
+
providerFactories: {
|
|
36
|
+
anthropic: () => {
|
|
37
|
+
const protocol = ctx.getWireProtocol('anthropic-messages')
|
|
38
|
+
if (!protocol) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"wire protocol 'anthropic-messages' is not registered — is extensions/plugin-otto-wire-protocols installed and trusted? (RFC-148)",
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
return protocol.create({
|
|
44
|
+
resolveAuth: () => ctx.resolveProviderAuth('anthropic').then((a) => a ?? undefined),
|
|
45
|
+
resolveBaseUrl: async () => 'https://api.anthropic.com',
|
|
46
|
+
headers: { 'anthropic-version': '2023-06-01' },
|
|
47
|
+
customizeRequest,
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}))
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
// RFC-148 M2:AnthropicSystemTextBlock(anthropic.ts 内部类型)已随协议实现物理迁出,
|
|
3
|
+
// 改用 @x-otto/provider 的协议中立泛化类型 WireSystemTextBlock(结构完全一致,见 D1 §types.ts)。
|
|
4
|
+
import type { WireSystemTextBlock } from '@x-otto/provider'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* anthropic-oauth-client-identity.ts —— 官方客户端形状匹配(订阅 OAuth token 专用)。
|
|
8
|
+
*
|
|
9
|
+
* RFC-140 架构下沉:此逻辑此前物理存在于 `@x-otto/provider`(通用契约包,`anthropic.ts`
|
|
10
|
+
* 内部硬编码调用),但它只对"首方 Anthropic 官方 API + 订阅 OAuth"这一具体场景成立且
|
|
11
|
+
* 必要——是 anthropic 一家厂商的专属行为,不该驻留在"多厂商共享的通用插入点"里。现通过
|
|
12
|
+
* `AnthropicProviderOptions.customizeRequest` 通用定制点(`packages/provider/src/provider/
|
|
13
|
+
* anthropic.ts`)注入,由本文件所在的装载层(`provider-loader.ts` `buildFactory`)在识别到
|
|
14
|
+
* `preserveProviderId===true`(首方插件信号)时构造并传入,`anthropic.ts` 本身不再认识
|
|
15
|
+
* "官方 CLI 客户端"这个具体身份概念。
|
|
16
|
+
*
|
|
17
|
+
* Anthropic 对订阅 OAuth token(`sk-ant-oat`,Claude Pro/Max)挂了一道客户端分类器:
|
|
18
|
+
* 请求必须「呈现为官方 CLI 客户端」,否则被激进限流——返回 `429 rate_limit_error`
|
|
19
|
+
* (注意非 `400`:格式合法、形状不对)。匹配点共三处:
|
|
20
|
+
* ① system 数组首块身份串;② 客户端请求头(x-app / User-Agent / 会话 id / beta);
|
|
21
|
+
* ③ body.metadata.user_id 结构。
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ 仅在 `auth.mode === 'oauth'` 下启用——API key、其它 provider 一律不动。
|
|
24
|
+
* ⚠️ 用订阅 token 驱动第三方客户端属 Anthropic ToS 灰区;另:Max-only 账户在 body 携带
|
|
25
|
+
* `tools` 时仍可能被路由到 overage 计费道而得 `400`(见 hermes#15080)。调用方自负。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** system 数组首块必须逐字等于此串(官方 CLI 客户端固定身份前缀)。 */
|
|
29
|
+
export const ANTHROPIC_OAUTH_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude."
|
|
30
|
+
|
|
31
|
+
/** 官方 CLI 客户端专属 beta,与 OAuth 的 `oauth-2025-04-20` 一并发。 */
|
|
32
|
+
const ANTHROPIC_OAUTH_CLIENT_BETA = 'claude-code-20250219'
|
|
33
|
+
|
|
34
|
+
/** User-Agent 版本——经 env 覆盖以跟随真实 CLI 版本(格式 `claude-cli/<v> (external, cli)`)。 */
|
|
35
|
+
const ANTHROPIC_OAUTH_CLIENT_VERSION = process.env['OTTO_ANTHROPIC_OAUTH_CLIENT_VERSION'] || '2.0.1'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 把 OAuth 的 beta 头(如 `oauth-2025-04-20`)与 `claude-code-20250219` 合并去重,
|
|
39
|
+
* 保留入参里的既有项与顺序。
|
|
40
|
+
*/
|
|
41
|
+
export function mergeOAuthBetaHeader(beta?: string): string {
|
|
42
|
+
const parts = (beta ? beta.split(',') : []).map((s) => s.trim()).filter(Boolean)
|
|
43
|
+
if (!parts.includes(ANTHROPIC_OAUTH_CLIENT_BETA)) {
|
|
44
|
+
parts.push(ANTHROPIC_OAUTH_CLIENT_BETA)
|
|
45
|
+
}
|
|
46
|
+
return parts.join(',')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 订阅 OAuth 请求的客户端伪装头:x-app / User-Agent / 会话 id + 合并后的 anthropic-beta。
|
|
51
|
+
* 经 SDK `defaultHeaders` 注入——位于平台头之后、authHeaders 之后,故能覆盖 SDK 默认
|
|
52
|
+
* `User-Agent: Anthropic/JS x.y.z`,且不会顶掉 Bearer 鉴权头。
|
|
53
|
+
*/
|
|
54
|
+
export function anthropicOAuthClientHeaders(
|
|
55
|
+
beta: string | undefined,
|
|
56
|
+
sessionId: string,
|
|
57
|
+
): Record<string, string> {
|
|
58
|
+
return {
|
|
59
|
+
'anthropic-beta': mergeOAuthBetaHeader(beta),
|
|
60
|
+
'x-app': 'cli',
|
|
61
|
+
'User-Agent': `claude-cli/${ANTHROPIC_OAUTH_CLIENT_VERSION} (external, cli)`,
|
|
62
|
+
'X-Claude-Code-Session-Id': sessionId,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let cachedDeviceId: string | undefined
|
|
67
|
+
/** 进程内稳定的 device_id(真实官方客户端会持久化;此处够分类器识别即可,可经 env 固定)。 */
|
|
68
|
+
function deviceId(): string {
|
|
69
|
+
if (!cachedDeviceId) {
|
|
70
|
+
cachedDeviceId = process.env['OTTO_ANTHROPIC_OAUTH_DEVICE_ID'] || randomUUID()
|
|
71
|
+
}
|
|
72
|
+
return cachedDeviceId
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** body.metadata.user_id——与真实官方客户端同构(device_id / account_uuid / session_id 的 JSON 串)。 */
|
|
76
|
+
export function anthropicOAuthRequestMetadata(sessionId: string, accountUuid = ''): { user_id: string } {
|
|
77
|
+
return {
|
|
78
|
+
user_id: JSON.stringify({
|
|
79
|
+
device_id: deviceId(),
|
|
80
|
+
account_uuid: accountUuid,
|
|
81
|
+
session_id: sessionId,
|
|
82
|
+
}),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 在 system 数组首部插入官方客户端身份块(幂等:已是首块则原样返回)。
|
|
88
|
+
* 插在最前 == 落在 `buildSystemBlocks` 的稳定前缀断点之前,故进同一缓存前缀。
|
|
89
|
+
*/
|
|
90
|
+
export function prependAnthropicOAuthIdentity(
|
|
91
|
+
blocks: WireSystemTextBlock[] | undefined,
|
|
92
|
+
): WireSystemTextBlock[] {
|
|
93
|
+
const prefix: WireSystemTextBlock = { type: 'text', text: ANTHROPIC_OAUTH_SYSTEM_PREFIX }
|
|
94
|
+
if (!blocks || blocks.length === 0) {
|
|
95
|
+
return [prefix]
|
|
96
|
+
}
|
|
97
|
+
if (blocks[0]?.text === ANTHROPIC_OAUTH_SYSTEM_PREFIX) {
|
|
98
|
+
return blocks
|
|
99
|
+
}
|
|
100
|
+
return [prefix, ...blocks]
|
|
101
|
+
}
|