@magpie-community/opencode-kiro-auth 0.1.19 → 0.1.20
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 +80 -1
- package/index.mjs +461 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,12 @@ request:
|
|
|
105
105
|
that can't be read or brought under is replaced by a short note, so the
|
|
106
106
|
conversation goes on (#428). Smaller images are sent as they are.
|
|
107
107
|
- Tools the history used but the request doesn't offer are declared.
|
|
108
|
+
- A tool's input schema says `type: "object"` at its root, as Kiro
|
|
109
|
+
wants: one that says no type (or object-or-null) is sent saying so, and
|
|
110
|
+
an empty one as an object with no properties. Kiro answers 400
|
|
111
|
+
`REQUEST_BODY_INVALID` to either. Everything under the root (a union
|
|
112
|
+
there, properties, `$ref`s, `nullable`) is sent as given, and a root
|
|
113
|
+
that names another type is left alone.
|
|
108
114
|
- It turns the AWS event stream Kiro answers with back into Messages'
|
|
109
115
|
events (streamed) or one message. The reply includes text, thinking,
|
|
110
116
|
tool calls and usage. When Kiro reports only how full the context is,
|
|
@@ -113,12 +119,86 @@ request:
|
|
|
113
119
|
the prompt, with a budget: low 10k, medium 20k, high 30k, xhigh and max
|
|
114
120
|
50k (as magpie's built-in Kiro: #120). The
|
|
115
121
|
`<thinking>` block of the reply comes back as thinking.
|
|
122
|
+
- Anthropic's hosted web search is answered with Kiro's own web search
|
|
123
|
+
(see [Web search](#web-search)).
|
|
116
124
|
- Kiro's failures keep their meaning:
|
|
117
125
|
- input too long → 400
|
|
118
126
|
- no capacity → 503
|
|
119
127
|
- usage limit → 429
|
|
120
128
|
- throttling → "rate limited"
|
|
121
129
|
|
|
130
|
+
## Web search
|
|
131
|
+
|
|
132
|
+
A request offering Anthropic's hosted web search,
|
|
133
|
+
`{"type": "web_search_20250305", "name": "web_search", "max_uses": n}`, is
|
|
134
|
+
searched with Kiro's own web search, sent as kiro-cli (2.35) sends it:
|
|
135
|
+
|
|
136
|
+
- The model is offered kiro-cli's `web_search` tool as kiro-cli's agent
|
|
137
|
+
offers it: its description, a `query` (200 characters at most) and the
|
|
138
|
+
`__tool_use_purpose` kiro-cli adds to each tool. A call the model makes to
|
|
139
|
+
it is searched here, never passed to the caller as a `tool_use`. Its
|
|
140
|
+
`__tool_use_purpose` is kiro-cli's alone: it isn't searched for, and the
|
|
141
|
+
caller's `server_tool_use` has the `query` only.
|
|
142
|
+
- The search is Kiro's runtime's `InvokeMCP` operation:
|
|
143
|
+
`POST runtime.<region>.kiro.dev/`, `Content-Type:
|
|
144
|
+
application/x-amz-json-1.0`, `x-amz-target:
|
|
145
|
+
AmazonCodeWhispererStreamingService.InvokeMCP`, in the account's region
|
|
146
|
+
with its token. Its body is a JSON-RPC `tools/call` of `web_search` with
|
|
147
|
+
`{query}` and nothing else, and the profile as `profileArn` beside it:
|
|
148
|
+
over AWS JSON 1.0 that member is sent in the body, and without it there
|
|
149
|
+
Kiro answers 400 `profileArn is required for this request`. It goes in
|
|
150
|
+
`x-amzn-kiro-profile-arn` too, as kiro-cli 2.35 sends it. An API key,
|
|
151
|
+
sent as `tokentype: API_KEY`, names no profile in either, as kiro-cli
|
|
152
|
+
names none for one. The answer is
|
|
153
|
+
`{jsonrpc, id, result, error}`, the result being the `tools/call`'s.
|
|
154
|
+
- The pages found go back to the model as the call's result (title, URL,
|
|
155
|
+
date, snippet), and it is asked again. The caller is answered as
|
|
156
|
+
Anthropic answers: `server_tool_use` and `web_search_tool_result` blocks
|
|
157
|
+
(streamed or not), then the answer, and `usage.server_tool_use.web_search_requests`.
|
|
158
|
+
- Each result has the page's `title`, `url` and `page_age`. `encrypted_content`
|
|
159
|
+
is `""`: Kiro has no sealed copy of the page, and none is made up (as
|
|
160
|
+
magpie's own search answers). The answer cites its pages by URL in its
|
|
161
|
+
text; it has no `citations`, which need Anthropic's encrypted index.
|
|
162
|
+
- `max_uses` is kept, and no request searches more than 10 times. A search
|
|
163
|
+
past it is a `max_uses_exceeded` error; a model still searching after
|
|
164
|
+
`max_uses` + 2 turns is stopped with `pause_turn`. A `max_uses` that isn't
|
|
165
|
+
a whole number of at least 1 is a 400.
|
|
166
|
+
- `allowed_domains` or `blocked_domains` filter the pages Kiro's search
|
|
167
|
+
returns; the search itself isn't told them, so fewer pages may come back.
|
|
168
|
+
A domain takes in its subdomains; with a path (`example.com/Docs`), only
|
|
169
|
+
pages under it, the host matched in any case and the path as written.
|
|
170
|
+
Both at once, or `user_location`, which Kiro's search has no place for,
|
|
171
|
+
is a 400.
|
|
172
|
+
- A search that fails (Kiro unreachable, an error, an answer it can't
|
|
173
|
+
read, a query over 200 characters) is a `web_search_tool_result_error`,
|
|
174
|
+
and the model is told why. A token Kiro's search turns down (403) is
|
|
175
|
+
refreshed once, as a request's is. A cancelled request stops searching.
|
|
176
|
+
- Kiro failing on a turn after a search is an error: streamed, an `error`
|
|
177
|
+
event after the blocks said; not streamed, the failure's status, not a
|
|
178
|
+
200 holding only the searches.
|
|
179
|
+
- A turn that also calls the caller's own tools ends there (`tool_use`),
|
|
180
|
+
with its searches. The conversation sent back, with its
|
|
181
|
+
`server_tool_use` and `web_search_tool_result` blocks, gives the model
|
|
182
|
+
each search's call and result again. A tool of the caller's named
|
|
183
|
+
`web_search` (no hosted `type`) stays the caller's.
|
|
184
|
+
- Other hosted tools are not sent, as before.
|
|
185
|
+
|
|
186
|
+
### Supported scope
|
|
187
|
+
|
|
188
|
+
Only `web_search_20250305` named `web_search` is searched (Claude Code sends
|
|
189
|
+
it). Any other tool whose type begins `web_search` (a newer version such as
|
|
190
|
+
`web_search_20260209`, another name, the same tool twice) is a 400 before
|
|
191
|
+
Kiro is asked: with `searches: true` magpie hands every such tool to the
|
|
192
|
+
plugin rather than searching for it, so dropping it would leave the search
|
|
193
|
+
asked for undone. A client that sends a newer version needs
|
|
194
|
+
`provider.kiro.searches: false`, so magpie's own search serves it as it did
|
|
195
|
+
before; the 400 says so.
|
|
196
|
+
|
|
197
|
+
The `config` hook says `searches: true` on the provider, so magpie passes a
|
|
198
|
+
client's web search (Claude Code's WebSearch) on to Kiro as sent rather than
|
|
199
|
+
searching for it. Set `provider.kiro.searches` to `false` to have magpie's
|
|
200
|
+
own search used instead.
|
|
201
|
+
|
|
122
202
|
## Models
|
|
123
203
|
|
|
124
204
|
The `config` hook declares `auto` (Kiro picks the model). Once signed in,
|
|
@@ -134,4 +214,3 @@ and Auto have the thinking variants.
|
|
|
134
214
|
provider.
|
|
135
215
|
- Signing in with a company's own identity provider in the browser (use
|
|
136
216
|
kiro-cli's sign-in).
|
|
137
|
-
- magpie's web-search stand-in for Kiro.
|
package/index.mjs
CHANGED
|
@@ -845,6 +845,21 @@ function formatText(req) {
|
|
|
845
845
|
return "Respond with a single JSON value and nothing else, matching this JSON schema:\n" + JSON.stringify(f.schema) + "."
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
+
// inputSchema is a tool's input schema as Kiro takes it: one whose root
|
|
849
|
+
// says type "object". Kiro answers 400 REQUEST_BODY_INVALID to a root with
|
|
850
|
+
// properties and no type (Haiku 5.5: the same schema with the type added
|
|
851
|
+
// is called), and to {}. A tool's input is always an object, so a root
|
|
852
|
+
// that says no type, or object-or-null, is said to be one; the rest of it
|
|
853
|
+
// (a union at its root, its properties, $refs, nullable) is sent as given,
|
|
854
|
+
// and a root that says another type is left as the caller said it. The
|
|
855
|
+
// caller's schema isn't changed: the root is copied.
|
|
856
|
+
function inputSchema(s) {
|
|
857
|
+
if (!s || typeof s !== "object" || Array.isArray(s) || !Object.keys(s).length) return { type: "object", properties: {} }
|
|
858
|
+
if (s.type === undefined) return { type: "object", ...s }
|
|
859
|
+
if (Array.isArray(s.type) && s.type.includes("object") && s.type.every((t) => t === "object" || t === "null")) return { ...s, type: "object" }
|
|
860
|
+
return s
|
|
861
|
+
}
|
|
862
|
+
|
|
848
863
|
// buildKiro is the body of a generateAssistantResponse call, as it is
|
|
849
864
|
// before its images are fitted (kiroBody).
|
|
850
865
|
function buildKiro(req, model, profile, budget) {
|
|
@@ -866,26 +881,53 @@ function kiroRequest(req, model, profile, budget) {
|
|
|
866
881
|
if (parts.some((p) => p?.cache_control)) marked.add(msg)
|
|
867
882
|
}
|
|
868
883
|
const user = () => ({ content: "", modelId: model, origin: "KIRO_CLI" })
|
|
884
|
+
const resultsOnly = new WeakSet() // the messages made to carry a reply's web search results
|
|
869
885
|
for (const m of req.messages ?? []) {
|
|
870
886
|
const parts = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content ?? []
|
|
871
887
|
const texts = []
|
|
872
888
|
if (m.role === "assistant") {
|
|
873
|
-
|
|
889
|
+
// a web search's result is in the reply itself (server_tool_use, then
|
|
890
|
+
// web_search_tool_result): to Kiro the reply up to it is a turn of its
|
|
891
|
+
// own, answered by a message carrying the result
|
|
892
|
+
let a = { content: "", toolUses: [] }
|
|
893
|
+
let last = null
|
|
894
|
+
const flush = () => {
|
|
895
|
+
a.content = texts.splice(0).join("\n\n")
|
|
896
|
+
if (!a.content && !a.toolUses.length) return // what only thought
|
|
897
|
+
const prev = entries.at(-1)?.assistantResponseMessage
|
|
898
|
+
if (prev) {
|
|
899
|
+
prev.content = joinNonEmpty(prev.content, a.content)
|
|
900
|
+
prev.toolUses.push(...a.toolUses)
|
|
901
|
+
} else entries.push({ assistantResponseMessage: a })
|
|
902
|
+
last = prev ?? a
|
|
903
|
+
a = { content: "", toolUses: [] }
|
|
904
|
+
}
|
|
874
905
|
for (const p of parts) {
|
|
875
906
|
if (p?.type === "text" && p.text) texts.push(p.text)
|
|
876
907
|
else if (p?.type === "tool_use") {
|
|
877
908
|
const input = p.input && typeof p.input === "object" && !Array.isArray(p.input) ? p.input : {}
|
|
878
909
|
a.toolUses.push({ name: toolName(p.name), toolUseId: toolID(p.id), input })
|
|
910
|
+
} else if (p?.type === "server_tool_use" && p.name === SEARCH_NAME) {
|
|
911
|
+
const input = p.input && typeof p.input === "object" && !Array.isArray(p.input) ? p.input : {}
|
|
912
|
+
a.toolUses.push({ name: SEARCH_NAME, toolUseId: toolID(p.id), input })
|
|
913
|
+
} else if (p?.type === "web_search_tool_result") {
|
|
914
|
+
flush()
|
|
915
|
+
const text = searchedText(p)
|
|
916
|
+
const result = { toolUseId: toolID(p.tool_use_id), status: text === null ? "error" : "success",
|
|
917
|
+
content: [{ text: text ?? searched.get(p.tool_use_id) ?? searchFailedText(p.content?.error_code) }] }
|
|
918
|
+
// parallel searches (both calls, then both results, as
|
|
919
|
+
// Anthropic answers them) are answered in one message
|
|
920
|
+
const prev = entries.at(-1)?.userInputMessage
|
|
921
|
+
if (prev && resultsOnly.has(prev)) prev.userInputMessageContext.toolResults.push(result)
|
|
922
|
+
else {
|
|
923
|
+
const u = { ...user(), images: [], userInputMessageContext: { toolResults: [result] } }
|
|
924
|
+
resultsOnly.add(u)
|
|
925
|
+
entries.push({ userInputMessage: u })
|
|
926
|
+
}
|
|
879
927
|
}
|
|
880
928
|
}
|
|
881
|
-
|
|
882
|
-
if (
|
|
883
|
-
const prev = entries.at(-1)?.assistantResponseMessage
|
|
884
|
-
if (prev) {
|
|
885
|
-
prev.content = joinNonEmpty(prev.content, a.content)
|
|
886
|
-
prev.toolUses.push(...a.toolUses)
|
|
887
|
-
} else entries.push({ assistantResponseMessage: a })
|
|
888
|
-
mark(prev ?? a, parts)
|
|
929
|
+
flush()
|
|
930
|
+
if (last) mark(last, parts)
|
|
889
931
|
continue
|
|
890
932
|
}
|
|
891
933
|
const u = user()
|
|
@@ -981,11 +1023,19 @@ function kiroRequest(req, model, profile, budget) {
|
|
|
981
1023
|
const tools = []
|
|
982
1024
|
const offered = new Set()
|
|
983
1025
|
const EMPTY = { type: "object", properties: {} }
|
|
1026
|
+
const hosted = hostedSearch(req)
|
|
984
1027
|
for (const t of req.tools ?? []) {
|
|
1028
|
+
if (hosted && t === hosted.tool) {
|
|
1029
|
+
// Kiro's own web search, run here when the model calls it (searching)
|
|
1030
|
+
offered.add(SEARCH_NAME)
|
|
1031
|
+
tools.push({ toolSpecification: SEARCH_SPEC })
|
|
1032
|
+
if (t.cache_control) tools.push({ cachePoint: { type: "default" } })
|
|
1033
|
+
continue
|
|
1034
|
+
}
|
|
985
1035
|
if (!t?.name || (t.type && t.type !== "custom" && !t.input_schema)) continue // server tools Kiro hasn't
|
|
986
1036
|
const name = toolName(t.name)
|
|
987
1037
|
offered.add(name)
|
|
988
|
-
tools.push({ toolSpecification: { name, description: t.description || t.name, inputSchema: { json: t.input_schema
|
|
1038
|
+
tools.push({ toolSpecification: { name, description: t.description || t.name, inputSchema: { json: inputSchema(t.input_schema) } } })
|
|
989
1039
|
if (t.cache_control) tools.push({ cachePoint: { type: "default" } })
|
|
990
1040
|
}
|
|
991
1041
|
for (const e of entries)
|
|
@@ -1016,21 +1066,24 @@ function kiroRequest(req, model, profile, budget) {
|
|
|
1016
1066
|
return out
|
|
1017
1067
|
}
|
|
1018
1068
|
|
|
1019
|
-
|
|
1069
|
+
// sdkHeaders are the AWS SDK's headers on a call to Kiro's runtime, as
|
|
1070
|
+
// kiro-cli's client sends them: who is calling, and as which client.
|
|
1071
|
+
function sdkHeaders(a) {
|
|
1020
1072
|
const ua = `aws-sdk-rust/1.0.0 ua/2.1 os/other lang/rust api/codewhispererstreaming#1.28.3 m/E app/AmazonQ-For-CLI md/appVersion-1.28.3-${randomUUID().replaceAll("-", "")}`
|
|
1073
|
+
return {
|
|
1074
|
+
...authHeaders(a),
|
|
1075
|
+
"amz-sdk-invocation-id": randomUUID(),
|
|
1076
|
+
"amz-sdk-request": "attempt=1; max=1",
|
|
1077
|
+
"x-amz-user-agent": ua,
|
|
1078
|
+
"User-Agent": ua,
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function sendKiro(a, body, signal) {
|
|
1021
1083
|
return fetch(runtime(a.region) + "/generateAssistantResponse", {
|
|
1022
1084
|
method: "POST",
|
|
1023
|
-
headers: {
|
|
1024
|
-
|
|
1025
|
-
"Content-Type": "application/json",
|
|
1026
|
-
Accept: "application/vnd.amazon.eventstream",
|
|
1027
|
-
"x-amzn-codewhisperer-optout": "true",
|
|
1028
|
-
"amz-sdk-invocation-id": randomUUID(),
|
|
1029
|
-
"amz-sdk-request": "attempt=1; max=1",
|
|
1030
|
-
"x-amzn-kiro-agent-mode": "vibe",
|
|
1031
|
-
"x-amz-user-agent": ua,
|
|
1032
|
-
"User-Agent": ua,
|
|
1033
|
-
},
|
|
1085
|
+
headers: { ...sdkHeaders(a), "x-amzn-codewhisperer-optout": "true", "x-amzn-kiro-agent-mode": "vibe",
|
|
1086
|
+
"Content-Type": "application/json", Accept: "application/vnd.amazon.eventstream" },
|
|
1034
1087
|
body,
|
|
1035
1088
|
signal,
|
|
1036
1089
|
})
|
|
@@ -1319,10 +1372,13 @@ function finished(content) {
|
|
|
1319
1372
|
// Kiro read from its cache is unknown, not 0: the cache fields are left
|
|
1320
1373
|
// out and magpie is told so (magpie_cache_unknown), so it shows no hit
|
|
1321
1374
|
// rate rather than 0%.
|
|
1322
|
-
const anthropicUsage = (u) =>
|
|
1323
|
-
u.reported
|
|
1375
|
+
const anthropicUsage = (u) => ({
|
|
1376
|
+
...(u.reported
|
|
1324
1377
|
? { input_tokens: u.input, output_tokens: u.output, cache_read_input_tokens: u.cacheRead, cache_creation_input_tokens: u.cacheWrite }
|
|
1325
|
-
: { input_tokens: u.input, output_tokens: u.output, magpie_cache_unknown: true }
|
|
1378
|
+
: { input_tokens: u.input, output_tokens: u.output, magpie_cache_unknown: true }),
|
|
1379
|
+
// the hosted web searches made (searching)
|
|
1380
|
+
...(u.searches ? { server_tool_use: { web_search_requests: u.searches } } : {}),
|
|
1381
|
+
})
|
|
1326
1382
|
|
|
1327
1383
|
// quotaWords are magpie's (internal/gateway/fallback.go): how a vendor says
|
|
1328
1384
|
// "out of quota" or "slow down".
|
|
@@ -1355,11 +1411,16 @@ async function reply(it, model, stream, names = new Map()) {
|
|
|
1355
1411
|
else add({ type: "thinking", thinking: e.text, signature: "" })
|
|
1356
1412
|
} else if (e.kind === "sig") {
|
|
1357
1413
|
if (last?.type === "thinking") last.signature += e.text
|
|
1358
|
-
} else if (e.kind === "
|
|
1414
|
+
} else if (e.kind === "serverTool") add({ type: "server_tool_use", id: e.id, name: e.name, input: e.input })
|
|
1415
|
+
else if (e.kind === "searched") add({ type: "web_search_tool_result", tool_use_id: e.id, content: e.content })
|
|
1416
|
+
else if (e.kind === "toolStart") add({ type: "tool_use", id: e.id, name: names.get(e.name) ?? e.name, input: "" })
|
|
1359
1417
|
else if (e.kind === "toolArgs") {
|
|
1360
1418
|
if (last?.type === "tool_use") last.input += e.text
|
|
1361
1419
|
} else if (e.kind === "error") {
|
|
1362
|
-
// with none of the answer said, a status; else what was said
|
|
1420
|
+
// with none of the answer said, a status; else what was said. A
|
|
1421
|
+
// turn after a web search that failed is a status even so: what
|
|
1422
|
+
// was said is the searches, without the answer they were for
|
|
1423
|
+
if (e.status) return errorResponse(e.status, e.text)
|
|
1363
1424
|
if (!content.length) return failedBefore(e.text)
|
|
1364
1425
|
return Response.json({ id, type: "message", role: "assistant", model, content: finished(content), stop_reason: "end_turn",
|
|
1365
1426
|
stop_sequence: null, usage: anthropicUsage({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }) })
|
|
@@ -1421,6 +1482,15 @@ async function reply(it, model, stream, names = new Map()) {
|
|
|
1421
1482
|
case "toolArgs":
|
|
1422
1483
|
if (open === "tool") ctl.enqueue(sse("content_block_delta", { index, delta: { type: "input_json_delta", partial_json: e.text } }))
|
|
1423
1484
|
break
|
|
1485
|
+
case "serverTool":
|
|
1486
|
+
begin("server", { type: "server_tool_use", id: e.id, name: e.name, input: {} })
|
|
1487
|
+
ctl.enqueue(sse("content_block_delta", { index, delta: { type: "input_json_delta", partial_json: JSON.stringify(e.input) } }))
|
|
1488
|
+
close()
|
|
1489
|
+
break
|
|
1490
|
+
case "searched":
|
|
1491
|
+
begin("searched", { type: "web_search_tool_result", tool_use_id: e.id, content: e.content })
|
|
1492
|
+
close()
|
|
1493
|
+
break
|
|
1424
1494
|
case "error":
|
|
1425
1495
|
close()
|
|
1426
1496
|
ctl.enqueue(sse("error", { error: { type: "api_error", message: e.text } }))
|
|
@@ -1439,6 +1509,222 @@ async function reply(it, model, stream, names = new Map()) {
|
|
|
1439
1509
|
return new Response(body, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } })
|
|
1440
1510
|
}
|
|
1441
1511
|
|
|
1512
|
+
// ---- web search: Kiro's own, for Anthropic's hosted tool ----------------------------
|
|
1513
|
+
//
|
|
1514
|
+
// A request offering Anthropic's hosted web search ({type:
|
|
1515
|
+
// "web_search_20250305", name: "web_search"}) has it searched by Kiro's own
|
|
1516
|
+
// web search, as kiro-cli (2.35) searches: its runtime's InvokeMCP
|
|
1517
|
+
// operation (AWS JSON 1.0, x-amz-target
|
|
1518
|
+
// AmazonCodeWhispererStreamingService.InvokeMCP, POSTed to the runtime's
|
|
1519
|
+
// root) carrying a JSON-RPC tools/call of web_search, with the account's
|
|
1520
|
+
// token and profile. The model is offered kiro-cli's web_search tool; a call
|
|
1521
|
+
// it makes to it is searched here, the pages found go back to it as the
|
|
1522
|
+
// call's result and it is asked again, and the caller is answered with
|
|
1523
|
+
// server_tool_use and web_search_tool_result blocks and the answer, as
|
|
1524
|
+
// Anthropic answers. The caller's own tools are never run here.
|
|
1525
|
+
|
|
1526
|
+
const SEARCH_TYPE = "web_search_20250305"
|
|
1527
|
+
const SEARCH_NAME = "web_search"
|
|
1528
|
+
const MCP_TARGET = "AmazonCodeWhispererStreamingService.InvokeMCP"
|
|
1529
|
+
const SEARCH_USES = 10 // searches one request may make at most, max_uses or not
|
|
1530
|
+
const SEARCH_QUERY = 200 // characters: what kiro-cli's web_search says its query takes
|
|
1531
|
+
const SEARCH_TIMEOUT = 30_000
|
|
1532
|
+
const SEARCH_BODY = 1 << 20 // bytes of a search's answer read at most
|
|
1533
|
+
const SEARCH_RESULTS = 20
|
|
1534
|
+
// PURPOSE is what kiro-cli's agent adds to every tool it offers: the model
|
|
1535
|
+
// says why it calls it. It isn't searched for, nor shown to the caller.
|
|
1536
|
+
const PURPOSE = "__tool_use_purpose"
|
|
1537
|
+
// kiro-cli's web_search, as its agent offers it (2.35's own strings)
|
|
1538
|
+
const SEARCH_SPEC = {
|
|
1539
|
+
name: SEARCH_NAME,
|
|
1540
|
+
description: "WebSearch looks up information that is outside the model's training data or cannot be reliably inferred from the current codebase/context.",
|
|
1541
|
+
inputSchema: { json: { type: "object", properties: {
|
|
1542
|
+
query: { type: "string", maxLength: SEARCH_QUERY, description: "Search query (max 200 chars) - use concise keywords, not full sentences" },
|
|
1543
|
+
[PURPOSE]: { type: "string", description: "A brief explanation why you are making this tool use." },
|
|
1544
|
+
}, required: ["query"] } },
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// hostedSearch is the request's hosted web search, as it asks for it: the
|
|
1548
|
+
// tool, how many searches it allows, and the domains it keeps or drops.
|
|
1549
|
+
// null when it offers none; { error } when it asks for what can't be done.
|
|
1550
|
+
function hostedSearch(req) {
|
|
1551
|
+
// magpie takes any tool whose type begins web_search for a web search and,
|
|
1552
|
+
// as this provider searches (config: searches), passes it here rather
|
|
1553
|
+
// than searching for it: one this can't search with is refused, not
|
|
1554
|
+
// dropped, so a search asked for isn't silently gone
|
|
1555
|
+
const searches = (req.tools ?? []).filter((t) => typeof t?.type === "string" && t.type.startsWith("web_search"))
|
|
1556
|
+
const odd = searches.find((t) => t.type !== SEARCH_TYPE || t.name !== SEARCH_NAME)
|
|
1557
|
+
if (odd) return { error: `the hosted web search on Kiro is {type: "${SEARCH_TYPE}", name: "${SEARCH_NAME}"}, not {type: ${JSON.stringify(odd.type)}, name: ${JSON.stringify(odd.name ?? null)}}; ` +
|
|
1558
|
+
"set provider.kiro.searches to false to have magpie search for it" }
|
|
1559
|
+
if (searches.length > 1) return { error: "web_search is offered more than once" }
|
|
1560
|
+
const tool = searches[0]
|
|
1561
|
+
if (!tool) return null
|
|
1562
|
+
if ((req.tools ?? []).some((t) => t !== tool && toolName(t?.name ?? "") === SEARCH_NAME))
|
|
1563
|
+
return { error: "two tools are named web_search: the hosted web search and another" }
|
|
1564
|
+
// Kiro's search has no place for where the user is
|
|
1565
|
+
if (tool.user_location) return { error: "web_search's user_location isn't supported on Kiro" }
|
|
1566
|
+
const list = (v) => (Array.isArray(v) ? v.filter((d) => typeof d === "string" && d.trim()).map((d) => d.trim()) : [])
|
|
1567
|
+
const allowed = list(tool.allowed_domains)
|
|
1568
|
+
const blocked = list(tool.blocked_domains)
|
|
1569
|
+
if (allowed.length && blocked.length) return { error: "web_search takes allowed_domains or blocked_domains, not both" }
|
|
1570
|
+
// a max_uses given is a whole number of at least 1, as Anthropic's API
|
|
1571
|
+
// takes it; none given is SEARCH_USES, and none is more
|
|
1572
|
+
if (tool.max_uses != null && !(Number.isInteger(tool.max_uses) && tool.max_uses > 0))
|
|
1573
|
+
return { error: "web_search's max_uses must be a whole number of at least 1" }
|
|
1574
|
+
return { tool, uses: Math.min(tool.max_uses ?? SEARCH_USES, SEARCH_USES), allowed, blocked }
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// a domain as Anthropic's allowed_domains and blocked_domains name one:
|
|
1578
|
+
// the host and its subdomains, and under a path when it gives one (a
|
|
1579
|
+
// host's case doesn't matter, a path's does)
|
|
1580
|
+
function inDomain(url, domain) {
|
|
1581
|
+
const slash = domain.indexOf("/")
|
|
1582
|
+
const host = (slash < 0 ? domain : domain.slice(0, slash)).replace(/^\*\./, "").toLowerCase()
|
|
1583
|
+
const path = slash < 0 ? "" : domain.slice(slash)
|
|
1584
|
+
const h = url.hostname.toLowerCase()
|
|
1585
|
+
return (h === host || h.endsWith("." + host)) && (!path || url.pathname.startsWith(path))
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// searched is what each search found, by its server_tool_use id, as the
|
|
1589
|
+
// model was given it: a conversation sent again with the result has its
|
|
1590
|
+
// snippets told the model again. Bounded; one not kept is told from the
|
|
1591
|
+
// result's titles and URLs.
|
|
1592
|
+
const searched = new Map()
|
|
1593
|
+
const SEARCHED_KEEP = 64
|
|
1594
|
+
const keepSearched = (id, text) => {
|
|
1595
|
+
searched.set(id, text)
|
|
1596
|
+
if (searched.size > SEARCHED_KEEP) searched.delete(searched.keys().next().value)
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const SEARCH_ERRORS = {
|
|
1600
|
+
invalid_tool_input: "the search query was missing or not text",
|
|
1601
|
+
query_too_long: `the search query was longer than ${SEARCH_QUERY} characters`,
|
|
1602
|
+
max_uses_exceeded: "no more searches are allowed for this request; answer with what was found",
|
|
1603
|
+
too_many_requests: "the search service is rate limited",
|
|
1604
|
+
unavailable: "the search service is unavailable",
|
|
1605
|
+
}
|
|
1606
|
+
const searchFailedText = (code) => "Web search failed: " + (SEARCH_ERRORS[code] ?? String(code ?? "unavailable"))
|
|
1607
|
+
|
|
1608
|
+
// searchedText is a web_search_tool_result told as text, for the model;
|
|
1609
|
+
// null when it is an error.
|
|
1610
|
+
function searchedText(p) {
|
|
1611
|
+
if (!Array.isArray(p?.content)) return null
|
|
1612
|
+
const kept = searched.get(p.tool_use_id)
|
|
1613
|
+
if (kept) return kept
|
|
1614
|
+
if (!p.content.length) return "No results."
|
|
1615
|
+
return p.content.filter((r) => r?.url).map((r, i) => `[${i + 1}] ${r.title || r.url}\nURL: ${r.url}` +
|
|
1616
|
+
(r.page_age ? `\nPublished: ${r.page_age}` : "")).join("\n\n")
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// secretless is a message with anything like a token taken out, and short.
|
|
1620
|
+
const secretless = (s) => String(s ?? "").replace(/Bearer\s+\S+/gi, "Bearer …").replace(/\b(ksk_|aoa|aor)[A-Za-z0-9._\-]{8,}/g, "…").slice(0, 200)
|
|
1621
|
+
|
|
1622
|
+
// bounded reads at most limit bytes of a body: null past it.
|
|
1623
|
+
async function bounded(res, limit) {
|
|
1624
|
+
if (!res.body) return ""
|
|
1625
|
+
const reader = res.body.getReader()
|
|
1626
|
+
const parts = []
|
|
1627
|
+
let n = 0
|
|
1628
|
+
for (;;) {
|
|
1629
|
+
const { done, value } = await reader.read()
|
|
1630
|
+
if (done) break
|
|
1631
|
+
n += value.length
|
|
1632
|
+
if (n > limit) {
|
|
1633
|
+
await reader.cancel().catch(() => {})
|
|
1634
|
+
return null
|
|
1635
|
+
}
|
|
1636
|
+
parts.push(value)
|
|
1637
|
+
}
|
|
1638
|
+
return new TextDecoder().decode(Buffer.concat(parts))
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// webSearch searches with Kiro's web_search through InvokeMCP: { results }
|
|
1642
|
+
// (Anthropic's web_search_result blocks) and the text the model is given,
|
|
1643
|
+
// or { code } (a web_search_tool_result_error's error_code) and why, or
|
|
1644
|
+
// { status: 403 } for a token Kiro turned down. An abort is thrown. The
|
|
1645
|
+
// profile is InvokeMCPRequest's profileArn, which AWS JSON 1.0 sends in the
|
|
1646
|
+
// body: without it there Kiro answers 400 "profileArn is required for this
|
|
1647
|
+
// request", the x-amzn-kiro-profile-arn header (its httpHeader binding in
|
|
1648
|
+
// Kiro's REST model, sent too, as kiro-cli 2.35 sends it) notwithstanding.
|
|
1649
|
+
// An API key names no profile, as kiro-cli names none for one. The tool's
|
|
1650
|
+
// arguments are its query alone.
|
|
1651
|
+
async function webSearch(a, query, hosted, signal) {
|
|
1652
|
+
const timeout = AbortSignal.timeout(SEARCH_TIMEOUT)
|
|
1653
|
+
const both = signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
1654
|
+
const body = { jsonrpc: "2.0", id: "1", method: "tools/call", ...(a.profile ? { profileArn: a.profile } : {}),
|
|
1655
|
+
params: { name: SEARCH_NAME, arguments: { query } } }
|
|
1656
|
+
let res, text
|
|
1657
|
+
try {
|
|
1658
|
+
res = await fetch(runtime(a.region) + "/", { method: "POST", body: JSON.stringify(body), signal: both,
|
|
1659
|
+
headers: { ...sdkHeaders(a), "amz-sdk-request": "attempt=1; max=3", "Content-Type": "application/x-amz-json-1.0",
|
|
1660
|
+
"x-amz-target": MCP_TARGET, ...(a.profile ? { "x-amzn-kiro-profile-arn": a.profile } : {}) } })
|
|
1661
|
+
if (res.status === 403) {
|
|
1662
|
+
await res.body?.cancel().catch(() => {})
|
|
1663
|
+
return { status: 403 }
|
|
1664
|
+
}
|
|
1665
|
+
text = await bounded(res, SEARCH_BODY)
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
if (signal?.aborted) throw e
|
|
1668
|
+
return { code: "unavailable", why: timeout.aborted ? "the search timed out" : "the search service couldn't be reached: " + secretless(e?.message) }
|
|
1669
|
+
}
|
|
1670
|
+
if (!res.ok) return { code: res.status === 429 ? "too_many_requests" : "unavailable", why: `the search service answered ${res.status}` }
|
|
1671
|
+
if (text === null) return { code: "unavailable", why: "the search's answer was too long" }
|
|
1672
|
+
return searchAnswer(text, hosted)
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// searchAnswer reads InvokeMCP's answer, { jsonrpc, id, result, error }: the
|
|
1676
|
+
// result is the tools/call's, its content a text block of the search's JSON.
|
|
1677
|
+
function searchAnswer(text, hosted) {
|
|
1678
|
+
let m
|
|
1679
|
+
try {
|
|
1680
|
+
m = JSON.parse(text)
|
|
1681
|
+
} catch {}
|
|
1682
|
+
const bad = (why) => ({ code: "unavailable", why })
|
|
1683
|
+
if (!m || typeof m !== "object") return bad("the search's answer wasn't JSON")
|
|
1684
|
+
if (m.error) return bad("the search failed: " + secretless(m.error.message ?? m.error.code))
|
|
1685
|
+
// a document, which may come as its JSON text
|
|
1686
|
+
let result = m.result
|
|
1687
|
+
if (typeof result === "string")
|
|
1688
|
+
try {
|
|
1689
|
+
result = JSON.parse(result)
|
|
1690
|
+
} catch {}
|
|
1691
|
+
const content = Array.isArray(result?.content) ? result.content : []
|
|
1692
|
+
const out = content.find((c) => c?.type === "text" && typeof c.text === "string")
|
|
1693
|
+
if (result?.isError) return bad("the search failed: " + secretless(out?.text))
|
|
1694
|
+
let r
|
|
1695
|
+
try {
|
|
1696
|
+
r = JSON.parse(out?.text ?? "")
|
|
1697
|
+
} catch {
|
|
1698
|
+
return bad("the search's answer wasn't one it reads")
|
|
1699
|
+
}
|
|
1700
|
+
if (!r || typeof r !== "object" || !Array.isArray(r.results)) return bad("the search's answer had no results list")
|
|
1701
|
+
if (r.error) return bad("the search failed: " + secretless(typeof r.error === "string" ? r.error : r.error.message))
|
|
1702
|
+
const results = []
|
|
1703
|
+
const lines = []
|
|
1704
|
+
for (const h of r.results) {
|
|
1705
|
+
let url
|
|
1706
|
+
try {
|
|
1707
|
+
url = new URL(String(h?.url ?? ""))
|
|
1708
|
+
} catch {
|
|
1709
|
+
continue
|
|
1710
|
+
}
|
|
1711
|
+
if (!/^https?:$/.test(url.protocol)) continue
|
|
1712
|
+
// Kiro's search has no domain filter: what it found outside the domains
|
|
1713
|
+
// asked for is left out
|
|
1714
|
+
if (hosted.allowed.length && !hosted.allowed.some((d) => inDomain(url, d))) continue
|
|
1715
|
+
if (hosted.blocked.some((d) => inDomain(url, d))) continue
|
|
1716
|
+
const title = typeof h.title === "string" && h.title.trim() ? h.title.trim() : url.href
|
|
1717
|
+
const age = typeof h.publishedDate === "string" && h.publishedDate ? h.publishedDate.slice(0, 64) : null
|
|
1718
|
+
// encrypted_content is the searcher's sealed copy of the page: Kiro
|
|
1719
|
+
// has none, and none is made up (as magpie's searchResultBlock)
|
|
1720
|
+
results.push({ type: "web_search_result", title, url: url.href, encrypted_content: "", page_age: age })
|
|
1721
|
+
const snippet = typeof h.snippet === "string" ? h.snippet.trim().slice(0, 2000) : ""
|
|
1722
|
+
lines.push(`[${results.length}] ${title}\nURL: ${url.href}` + (age ? `\nPublished: ${age}` : "") + (snippet ? `\n${snippet}` : ""))
|
|
1723
|
+
if (results.length >= SEARCH_RESULTS) break
|
|
1724
|
+
}
|
|
1725
|
+
return { results, text: lines.length ? lines.join("\n\n") : "No results." }
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1442
1728
|
// generate answers a Messages request through Kiro's API: once more,
|
|
1443
1729
|
// refreshed, when Kiro turns the token down (403). As the built-in: no
|
|
1444
1730
|
// credentials is a 401, a failed refresh after the 403 a 502; none marks
|
|
@@ -1446,39 +1732,176 @@ async function reply(it, model, stream, names = new Map()) {
|
|
|
1446
1732
|
async function generate(creds, auth, req, signal) {
|
|
1447
1733
|
const model = String(req.model ?? "auto")
|
|
1448
1734
|
const budget = thinking(req, model)
|
|
1735
|
+
const hosted = hostedSearch(req)
|
|
1736
|
+
if (hosted?.error) return errorResponse(400, hosted.error)
|
|
1737
|
+
const first = await ask(creds, auth, req, model, budget, signal)
|
|
1738
|
+
if (!first.res) return errorResponse(first.status, first.message)
|
|
1739
|
+
const it = hosted ? searching(creds, auth, req, model, budget, signal, hosted, first) : events(first.res.body, model, budget)
|
|
1740
|
+
return kept(await reply(it, model, req.stream === true, toolNames(req)))
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// ask sends one turn to Kiro: { res, a }, a 200, or the { status, message }
|
|
1744
|
+
// of a failure.
|
|
1745
|
+
async function ask(creds, auth, req, model, budget, signal) {
|
|
1449
1746
|
let a
|
|
1450
1747
|
try {
|
|
1451
1748
|
a = await creds(auth)
|
|
1452
1749
|
} catch (e) {
|
|
1453
|
-
return
|
|
1750
|
+
return { status: 401, message: e.message }
|
|
1454
1751
|
}
|
|
1455
1752
|
let res
|
|
1456
1753
|
try {
|
|
1457
1754
|
res = await sendKiro(a, await kiroBody(req, model, a.profile, budget), signal)
|
|
1458
1755
|
} catch (e) {
|
|
1459
1756
|
if (e?.name === "AbortError") throw e
|
|
1460
|
-
return
|
|
1757
|
+
return { status: 502, message: e.message }
|
|
1461
1758
|
}
|
|
1462
1759
|
if (res.status === 403) {
|
|
1463
1760
|
await res.body?.cancel()
|
|
1464
1761
|
try {
|
|
1465
1762
|
a = await creds(auth, true)
|
|
1466
1763
|
} catch (e) {
|
|
1467
|
-
return
|
|
1764
|
+
return { status: 502, message: e.message }
|
|
1468
1765
|
}
|
|
1469
1766
|
try {
|
|
1470
1767
|
res = await sendKiro(a, await kiroBody(req, model, a.profile, budget), signal)
|
|
1471
1768
|
} catch (e) {
|
|
1472
1769
|
if (e?.name === "AbortError") throw e
|
|
1473
|
-
return
|
|
1770
|
+
return { status: 502, message: e.message }
|
|
1474
1771
|
}
|
|
1475
1772
|
}
|
|
1476
1773
|
if (!res.ok) {
|
|
1477
1774
|
// Kiro's own 401 is passed on, as the built-in did, the sign-in kept
|
|
1478
|
-
|
|
1479
|
-
|
|
1775
|
+
return failure(res.status, (await res.text()).slice(0, 1 << 20))
|
|
1776
|
+
}
|
|
1777
|
+
return { res, a }
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const aborted = () => Object.assign(new Error("the request was cancelled"), { name: "AbortError" })
|
|
1781
|
+
|
|
1782
|
+
// searching is events for a request offering the hosted web search, over
|
|
1783
|
+
// as many turns as its searches take: a call the model makes to it is
|
|
1784
|
+
// searched rather than passed on (serverTool, searched), and Kiro is asked
|
|
1785
|
+
// again with what was found, until it answers, calls a tool of the
|
|
1786
|
+
// caller's (passed on as ever, the searches of its turn with it), or has
|
|
1787
|
+
// taken hosted.uses + 2 turns (pause_turn, which the caller may send back
|
|
1788
|
+
// to go on). Searches past hosted.uses are max_uses_exceeded.
|
|
1789
|
+
async function* searching(creds, auth, req, model, budget, signal, hosted, first) {
|
|
1790
|
+
let { res, a } = first
|
|
1791
|
+
const messages = [...(req.messages ?? [])]
|
|
1792
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reported: false, searches: 0 }
|
|
1793
|
+
let refreshed = false
|
|
1794
|
+
for (let turn = 1; ; turn++) {
|
|
1795
|
+
const said = [] // this turn's blocks, for the next turn's history
|
|
1796
|
+
const calls = [] // its searches
|
|
1797
|
+
let client = false
|
|
1798
|
+
let current = null
|
|
1799
|
+
let stop = null
|
|
1800
|
+
for await (const e of events(res.body, model, budget)) {
|
|
1801
|
+
if (e.kind === "toolStart") {
|
|
1802
|
+
current = e.name === SEARCH_NAME ? { args: "" } : null
|
|
1803
|
+
if (current) calls.push(current)
|
|
1804
|
+
else {
|
|
1805
|
+
client = true
|
|
1806
|
+
said.push({ type: "tool_use", id: e.id, name: e.name, input: "" })
|
|
1807
|
+
yield e
|
|
1808
|
+
}
|
|
1809
|
+
} else if (e.kind === "toolArgs") {
|
|
1810
|
+
if (current) current.args += e.text
|
|
1811
|
+
else {
|
|
1812
|
+
const b = said.at(-1)
|
|
1813
|
+
if (b?.type === "tool_use") b.input += e.text
|
|
1814
|
+
yield e
|
|
1815
|
+
}
|
|
1816
|
+
} else if (e.kind === "stop") stop = e
|
|
1817
|
+
else {
|
|
1818
|
+
if (e.kind === "text") {
|
|
1819
|
+
const b = said.at(-1)
|
|
1820
|
+
if (b?.type === "text") b.text += e.text
|
|
1821
|
+
else said.push({ type: "text", text: e.text })
|
|
1822
|
+
}
|
|
1823
|
+
if (e.kind === "error") {
|
|
1824
|
+
// in a turn after a search, a status, not a finished answer (reply)
|
|
1825
|
+
yield turn > 1 ? { ...e, status: 502 } : e
|
|
1826
|
+
return
|
|
1827
|
+
}
|
|
1828
|
+
yield e
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
if (signal?.aborted) throw aborted()
|
|
1832
|
+
if (!stop) return
|
|
1833
|
+
usage.input += stop.usage.input
|
|
1834
|
+
usage.output += stop.usage.output
|
|
1835
|
+
usage.cacheRead += stop.usage.cacheRead
|
|
1836
|
+
usage.cacheWrite += stop.usage.cacheWrite
|
|
1837
|
+
usage.reported ||= stop.usage.reported
|
|
1838
|
+
for (const b of said)
|
|
1839
|
+
if (b.type === "tool_use")
|
|
1840
|
+
try {
|
|
1841
|
+
b.input = b.input ? JSON.parse(b.input) : {}
|
|
1842
|
+
} catch {
|
|
1843
|
+
b.input = {}
|
|
1844
|
+
}
|
|
1845
|
+
if (!calls.length) {
|
|
1846
|
+
yield { ...stop, usage }
|
|
1847
|
+
return
|
|
1848
|
+
}
|
|
1849
|
+
for (const c of calls) {
|
|
1850
|
+
const id = "srvtoolu_" + randomBytes(12).toString("hex")
|
|
1851
|
+
let input = {}
|
|
1852
|
+
try {
|
|
1853
|
+
const v = c.args ? JSON.parse(c.args) : {}
|
|
1854
|
+
if (v && typeof v === "object" && !Array.isArray(v)) input = v
|
|
1855
|
+
} catch {}
|
|
1856
|
+
// why the model searched (PURPOSE) is kiro-cli's, not Anthropic's: the
|
|
1857
|
+
// caller sees the query alone, and only it is searched for
|
|
1858
|
+
const { [PURPOSE]: _why, ...shown } = input
|
|
1859
|
+
input = shown
|
|
1860
|
+
yield { kind: "serverTool", id, name: SEARCH_NAME, input }
|
|
1861
|
+
const query = typeof input.query === "string" ? input.query.trim() : ""
|
|
1862
|
+
let found
|
|
1863
|
+
if (!query) found = { code: "invalid_tool_input" }
|
|
1864
|
+
else if (query.length > SEARCH_QUERY) found = { code: "query_too_long" }
|
|
1865
|
+
else if (usage.searches >= hosted.uses) found = { code: "max_uses_exceeded" }
|
|
1866
|
+
else {
|
|
1867
|
+
usage.searches++
|
|
1868
|
+
found = await webSearch(a, query, hosted, signal)
|
|
1869
|
+
// a token Kiro turned down is refreshed once for the request, as a
|
|
1870
|
+
// turn's is
|
|
1871
|
+
if (found.status === 403 && !refreshed) {
|
|
1872
|
+
refreshed = true
|
|
1873
|
+
try {
|
|
1874
|
+
a = await creds(auth, true)
|
|
1875
|
+
found = await webSearch(a, query, hosted, signal)
|
|
1876
|
+
} catch (e) {
|
|
1877
|
+
if (e?.name === "AbortError" || signal?.aborted) throw e
|
|
1878
|
+
found = { code: "unavailable", why: secretless(e?.message) }
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (found.status === 403) found = { code: "unavailable", why: "Kiro turned down the token for its web search (403)" }
|
|
1882
|
+
}
|
|
1883
|
+
const content = found.results ?? { type: "web_search_tool_result_error", error_code: found.code }
|
|
1884
|
+
keepSearched(id, found.results ? found.text : searchFailedText(found.code) + (found.why ? ` (${found.why})` : ""))
|
|
1885
|
+
yield { kind: "searched", id, content }
|
|
1886
|
+
said.push({ type: "server_tool_use", id, name: SEARCH_NAME, input }, { type: "web_search_tool_result", tool_use_id: id, content })
|
|
1887
|
+
}
|
|
1888
|
+
if (signal?.aborted) throw aborted()
|
|
1889
|
+
if (client) {
|
|
1890
|
+
yield { kind: "stop", reason: "tool_use", usage }
|
|
1891
|
+
return
|
|
1892
|
+
}
|
|
1893
|
+
if (turn >= hosted.uses + 2) {
|
|
1894
|
+
yield { kind: "stop", reason: "pause_turn", usage }
|
|
1895
|
+
return
|
|
1896
|
+
}
|
|
1897
|
+
messages.push({ role: "assistant", content: said })
|
|
1898
|
+
const next = await ask(creds, auth, { ...req, messages }, model, budget, signal)
|
|
1899
|
+
if (!next.res) {
|
|
1900
|
+
yield { kind: "error", status: next.status, text: `Kiro, after its web search: ${next.message} (${next.status})` }
|
|
1901
|
+
return
|
|
1902
|
+
}
|
|
1903
|
+
;({ res, a } = next)
|
|
1480
1904
|
}
|
|
1481
|
-
return kept(await reply(events(res.body, model, budget), model, req.stream === true, toolNames(req)))
|
|
1482
1905
|
}
|
|
1483
1906
|
|
|
1484
1907
|
// ---- signing in ----------------------------------------------------------------------
|
|
@@ -1727,6 +2150,10 @@ export async function KiroAuthPlugin({ client } = {}) {
|
|
|
1727
2150
|
name: "Kiro",
|
|
1728
2151
|
npm: MESSAGES,
|
|
1729
2152
|
api: BASE,
|
|
2153
|
+
// magpie's own field (#2027): Kiro answers Anthropic's hosted web
|
|
2154
|
+
// search itself (searching), so magpie passes a client's on as sent
|
|
2155
|
+
// rather than searching for it. The user's own setting goes over it.
|
|
2156
|
+
searches: true,
|
|
1730
2157
|
...was,
|
|
1731
2158
|
models: { ...Object.fromEntries(MODELS.map((m) => [m.id, configModel(m)])), ...(was.models ?? {}) },
|
|
1732
2159
|
}
|