clanka 0.6.2 → 0.7.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/dist/Xai.d.ts +19 -0
- package/dist/Xai.d.ts.map +1 -0
- package/dist/Xai.js +40 -0
- package/dist/Xai.js.map +1 -0
- package/dist/Xai.test.d.ts +2 -0
- package/dist/Xai.test.d.ts.map +1 -0
- package/dist/Xai.test.js +105 -0
- package/dist/Xai.test.js.map +1 -0
- package/dist/XaiAuth.d.ts +43 -0
- package/dist/XaiAuth.d.ts.map +1 -0
- package/dist/XaiAuth.js +172 -0
- package/dist/XaiAuth.js.map +1 -0
- package/dist/XaiAuth.test.d.ts +2 -0
- package/dist/XaiAuth.test.d.ts.map +1 -0
- package/dist/XaiAuth.test.js +354 -0
- package/dist/XaiAuth.test.js.map +1 -0
- package/dist/cli.js +23 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Xai.test.ts +160 -0
- package/src/Xai.ts +57 -0
- package/src/XaiAuth.test.ts +495 -0
- package/src/XaiAuth.ts +272 -0
- package/src/cli.ts +30 -22
- package/src/index.ts +5 -0
package/src/Xai.test.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { assert, describe, it } from "@effect/vitest"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Exit from "effect/Exit"
|
|
4
|
+
import * as Layer from "effect/Layer"
|
|
5
|
+
import * as Stream from "effect/Stream"
|
|
6
|
+
import * as LanguageModel from "effect/unstable/ai/LanguageModel"
|
|
7
|
+
import * as Model from "effect/unstable/ai/Model"
|
|
8
|
+
import {
|
|
9
|
+
HttpClient,
|
|
10
|
+
type HttpClientRequest,
|
|
11
|
+
HttpClientResponse,
|
|
12
|
+
} from "effect/unstable/http"
|
|
13
|
+
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
|
|
14
|
+
import * as Compaction from "./Compaction.ts"
|
|
15
|
+
import { DeviceCodeHandler } from "./DeviceCodeHandler.ts"
|
|
16
|
+
import * as Public from "./index.ts"
|
|
17
|
+
import * as Xai from "./Xai.ts"
|
|
18
|
+
import { TokenData, toTokenStore } from "./XaiAuth.ts"
|
|
19
|
+
|
|
20
|
+
const capture = () => {
|
|
21
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
22
|
+
const client = HttpClient.make((request) =>
|
|
23
|
+
Effect.sync(() => {
|
|
24
|
+
requests.push(request)
|
|
25
|
+
return HttpClientResponse.fromWeb(
|
|
26
|
+
request,
|
|
27
|
+
new Response(
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
error: {
|
|
30
|
+
message: "transport probe",
|
|
31
|
+
type: "invalid_request_error",
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
{ status: 400, headers: { "content-type": "application/json" } },
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
return { requests, layer: Layer.succeed(HttpClient.HttpClient, client) }
|
|
40
|
+
}
|
|
41
|
+
const body = (
|
|
42
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
43
|
+
): Record<string, unknown> => {
|
|
44
|
+
if (request.body._tag !== "Uint8Array") throw new Error("Expected JSON body")
|
|
45
|
+
return JSON.parse(new TextDecoder().decode(request.body.body))
|
|
46
|
+
}
|
|
47
|
+
const seed = Effect.gen(function* () {
|
|
48
|
+
yield* toTokenStore(yield* KeyValueStore.KeyValueStore)
|
|
49
|
+
.set(
|
|
50
|
+
"token",
|
|
51
|
+
new TokenData({
|
|
52
|
+
access: "subscription-token",
|
|
53
|
+
refresh: "refresh",
|
|
54
|
+
expires: Date.now() + 3600000,
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
.pipe(Effect.orDie)
|
|
58
|
+
})
|
|
59
|
+
const noLogin = Layer.succeed(DeviceCodeHandler, {
|
|
60
|
+
onCode: () => Effect.die("Cached credentials must not prompt"),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("Xai provider", () => {
|
|
64
|
+
it("exports Xai, but not XaiAuth, from the public entry point", () => {
|
|
65
|
+
assert.strictEqual(Public.Xai, Xai)
|
|
66
|
+
assert.isFalse("XaiAuth" in Public)
|
|
67
|
+
assert.strictEqual(Xai.model("grok-4.6").provider, "xai")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
for (const effort of [undefined, "low"] as const) {
|
|
71
|
+
it.effect(
|
|
72
|
+
"uses authenticated HTTP Responses with " +
|
|
73
|
+
(effort ?? "default high") +
|
|
74
|
+
" reasoning",
|
|
75
|
+
() =>
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
yield* seed
|
|
78
|
+
const http = capture()
|
|
79
|
+
yield* Effect.gen(function* () {
|
|
80
|
+
assert.lengthOf(
|
|
81
|
+
http.requests,
|
|
82
|
+
0,
|
|
83
|
+
"Client construction must not trigger login or inference",
|
|
84
|
+
)
|
|
85
|
+
assert.strictEqual(yield* Model.ProviderName, "xai")
|
|
86
|
+
assert.strictEqual(yield* Model.ModelName, "grok-4.6")
|
|
87
|
+
const ai = yield* LanguageModel.LanguageModel
|
|
88
|
+
const exit = yield* ai
|
|
89
|
+
.streamText({ prompt: "hello" })
|
|
90
|
+
.pipe(Stream.runDrain, Effect.exit)
|
|
91
|
+
assert.isTrue(Exit.isFailure(exit))
|
|
92
|
+
}).pipe(
|
|
93
|
+
Effect.provide(
|
|
94
|
+
Xai.model(
|
|
95
|
+
"grok-4.6",
|
|
96
|
+
effort === undefined ? undefined : { reasoning: { effort } },
|
|
97
|
+
).pipe(
|
|
98
|
+
Layer.provide(Xai.layerClient),
|
|
99
|
+
Layer.provide(http.layer),
|
|
100
|
+
Layer.provide(noLogin),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
assert.lengthOf(http.requests, 1)
|
|
105
|
+
const request = http.requests[0]!
|
|
106
|
+
assert.strictEqual(request.method, "POST")
|
|
107
|
+
assert.strictEqual(request.url, "https://api.x.ai/v1/responses")
|
|
108
|
+
assert.strictEqual(
|
|
109
|
+
request.headers.authorization,
|
|
110
|
+
"Bearer subscription-token",
|
|
111
|
+
)
|
|
112
|
+
assert.match(request.headers["user-agent"]!, /clanka/i)
|
|
113
|
+
assert.notMatch(request.headers["user-agent"]!, /opencode/i)
|
|
114
|
+
assert.deepInclude(body(request), {
|
|
115
|
+
model: "grok-4.6",
|
|
116
|
+
store: false,
|
|
117
|
+
stream: true,
|
|
118
|
+
})
|
|
119
|
+
assert.deepInclude(body(request).reasoning, {
|
|
120
|
+
effort: effort ?? "high",
|
|
121
|
+
})
|
|
122
|
+
assert.isFalse("max_output_tokens" in body(request))
|
|
123
|
+
}).pipe(Effect.provide(KeyValueStore.layerMemory)),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
it.effect(
|
|
128
|
+
"caps summaries at 4k without leaking the cap into subsequent requests",
|
|
129
|
+
() =>
|
|
130
|
+
Effect.gen(function* () {
|
|
131
|
+
yield* seed
|
|
132
|
+
const http = capture()
|
|
133
|
+
yield* Effect.gen(function* () {
|
|
134
|
+
const ai = yield* LanguageModel.LanguageModel
|
|
135
|
+
const summarize = yield* Compaction.SummarizerTransform
|
|
136
|
+
yield* summarize(
|
|
137
|
+
ai.streamText({ prompt: "summarize" }).pipe(Stream.runDrain),
|
|
138
|
+
).pipe(Effect.exit)
|
|
139
|
+
yield* ai
|
|
140
|
+
.streamText({ prompt: "continue" })
|
|
141
|
+
.pipe(Stream.runDrain, Effect.exit)
|
|
142
|
+
}).pipe(
|
|
143
|
+
Effect.provide(
|
|
144
|
+
Xai.model("grok-4.6").pipe(
|
|
145
|
+
Layer.provide(Xai.layerClient),
|
|
146
|
+
Layer.provide(http.layer),
|
|
147
|
+
Layer.provide(noLogin),
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
assert.lengthOf(http.requests, 2)
|
|
152
|
+
assert.strictEqual(body(http.requests[0]!).max_output_tokens, 4000)
|
|
153
|
+
assert.isFalse("max_output_tokens" in body(http.requests[1]!))
|
|
154
|
+
for (const request of http.requests) {
|
|
155
|
+
assert.strictEqual(request.url, "https://api.x.ai/v1/responses")
|
|
156
|
+
assert.strictEqual(body(request).store, false)
|
|
157
|
+
}
|
|
158
|
+
}).pipe(Effect.provide(KeyValueStore.layerMemory)),
|
|
159
|
+
)
|
|
160
|
+
})
|
package/src/Xai.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 1.0.0
|
|
3
|
+
*/
|
|
4
|
+
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
|
|
5
|
+
import * as Layer from "effect/Layer"
|
|
6
|
+
import * as Struct from "effect/Struct"
|
|
7
|
+
import { XaiAuth } from "./XaiAuth.ts"
|
|
8
|
+
import { AgentModelConfig } from "./Agent.ts"
|
|
9
|
+
import * as Compaction from "./Compaction.ts"
|
|
10
|
+
import * as Model from "effect/unstable/ai/Model"
|
|
11
|
+
import type * as LanguageModel from "effect/unstable/ai/LanguageModel"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @since 1.0.0
|
|
15
|
+
* @category Layers
|
|
16
|
+
*/
|
|
17
|
+
export const layerClient = OpenAiClient.layer({
|
|
18
|
+
apiUrl: "https://api.x.ai/v1",
|
|
19
|
+
}).pipe(Layer.provide(XaiAuth.layerClient))
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @since 1.0.0
|
|
23
|
+
* @category Layers
|
|
24
|
+
*/
|
|
25
|
+
export const model = (
|
|
26
|
+
model: string,
|
|
27
|
+
options?:
|
|
28
|
+
| (OpenAiLanguageModel.Config["Service"] & typeof AgentModelConfig.Service)
|
|
29
|
+
| undefined,
|
|
30
|
+
): Model.Model<"xai", LanguageModel.LanguageModel, OpenAiClient.OpenAiClient> =>
|
|
31
|
+
Model.make(
|
|
32
|
+
"xai",
|
|
33
|
+
model,
|
|
34
|
+
Layer.mergeAll(
|
|
35
|
+
OpenAiLanguageModel.layer({
|
|
36
|
+
model,
|
|
37
|
+
config: {
|
|
38
|
+
...Struct.omit(options ?? {}, [
|
|
39
|
+
"systemPromptTransform",
|
|
40
|
+
"supportsImages",
|
|
41
|
+
]),
|
|
42
|
+
store: false,
|
|
43
|
+
reasoning: { effort: "high", ...options?.reasoning },
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
AgentModelConfig.layer({
|
|
47
|
+
systemPromptTransform: options?.systemPromptTransform,
|
|
48
|
+
supportsImages: options?.supportsImages,
|
|
49
|
+
}),
|
|
50
|
+
// Cap compaction summaries; xAI honours max_output_tokens.
|
|
51
|
+
Layer.succeed(Compaction.SummarizerTransform, (effect) =>
|
|
52
|
+
OpenAiLanguageModel.withConfigOverride(effect, {
|
|
53
|
+
max_output_tokens: Compaction.summarizerMaxOutputTokens,
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
)
|