@vellumai/assistant 0.12.5-dev.202609252314.2f8433a → 0.12.5-dev.202609260125.8a6a403

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.5-dev.202609252314.2f8433a",
3
+ "version": "0.12.5-dev.202609260125.8a6a403",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,12 +23,14 @@ export function writeDesktopChromePolicy(policyDir: string): void {
23
23
  }
24
24
  }
25
25
  if (
26
+ policy.NetworkPredictionOptions === 2 &&
26
27
  policy.CommandLineFlagSecurityWarningsEnabled === false &&
27
28
  policy.PasswordManagerEnabled === false
28
29
  ) {
29
30
  return;
30
31
  }
31
32
 
33
+ policy.NetworkPredictionOptions = 2;
32
34
  // This suppresses command-line warnings; Chrome's sandbox stays unchanged.
33
35
  policy.CommandLineFlagSecurityWarningsEnabled = false;
34
36
  // Suppress save prompts while keeping previously saved passwords usable.
@@ -0,0 +1,168 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import { setConfig } from "../../__tests__/helpers/set-config.js";
4
+ import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
5
+ import { RetryProvider } from "../retry.js";
6
+ import type { Message, SendMessageOptions } from "../types.js";
7
+ import {
8
+ FIREWORKS_SESSION_AFFINITY_HEADER,
9
+ FireworksProvider,
10
+ resolveFireworksRequestHeaders,
11
+ } from "./client.js";
12
+
13
+ const USER_TURN: Message[] = [
14
+ { role: "user", content: [{ type: "text", text: "question" }] },
15
+ ];
16
+
17
+ interface WireCall {
18
+ params: Record<string, unknown>;
19
+ headers: Record<string, string> | undefined;
20
+ }
21
+
22
+ /** Replace the SDK client so each chat-completions call is recorded. */
23
+ function stubChatCompletions(provider: OpenAIChatCompletionsProvider) {
24
+ const calls: WireCall[] = [];
25
+ (provider as unknown as { client: unknown }).client = {
26
+ baseURL: "https://api.fireworks.example/inference/v1",
27
+ chat: {
28
+ completions: {
29
+ create: async (
30
+ params: Record<string, unknown>,
31
+ options?: { headers?: Record<string, string> },
32
+ ) => {
33
+ calls.push({ params, headers: options?.headers });
34
+ return {
35
+ async *[Symbol.asyncIterator]() {
36
+ yield {
37
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
38
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
39
+ };
40
+ },
41
+ };
42
+ },
43
+ },
44
+ },
45
+ };
46
+ return calls;
47
+ }
48
+
49
+ function fireworksPipeline() {
50
+ const inner = new FireworksProvider(
51
+ "fw-test",
52
+ "accounts/fireworks/models/example-model",
53
+ );
54
+ const calls = stubChatCompletions(inner);
55
+ return { provider: new RetryProvider(inner), calls };
56
+ }
57
+
58
+ async function send(
59
+ provider: RetryProvider,
60
+ config: SendMessageOptions["config"],
61
+ ): Promise<void> {
62
+ await provider.sendMessage(USER_TURN, { config });
63
+ }
64
+
65
+ beforeEach(() => {
66
+ setConfig("llm", {});
67
+ });
68
+
69
+ describe("resolveFireworksRequestHeaders", () => {
70
+ test("sets the session affinity header from the key", () => {
71
+ expect(resolveFireworksRequestHeaders("conv-xyz")).toEqual({
72
+ [FIREWORKS_SESSION_AFFINITY_HEADER]: "conv-xyz",
73
+ });
74
+ });
75
+
76
+ test("omits the header without a usable key", () => {
77
+ expect(resolveFireworksRequestHeaders(undefined)).toEqual({});
78
+ expect(resolveFireworksRequestHeaders(" ")).toEqual({});
79
+ });
80
+ });
81
+
82
+ describe("Fireworks session affinity on the wire", () => {
83
+ test("sends the selectionSeed as x-session-affinity and keeps it out of the body", async () => {
84
+ const { provider, calls } = fireworksPipeline();
85
+
86
+ await send(provider, {
87
+ selectionSeed: "conv-seed",
88
+ conversationId: "conv-other",
89
+ });
90
+
91
+ expect(calls).toHaveLength(1);
92
+ expect(calls[0]!.headers?.[FIREWORKS_SESSION_AFFINITY_HEADER]).toBe(
93
+ "conv-seed",
94
+ );
95
+ const body = JSON.stringify(calls[0]!.params);
96
+ expect(body).not.toContain(FIREWORKS_SESSION_AFFINITY_HEADER);
97
+ expect(body).not.toContain("conv-seed");
98
+ expect(body).not.toContain("conv-other");
99
+ expect(calls[0]!.params).not.toHaveProperty("requestHeaders");
100
+ expect(calls[0]!.params).not.toHaveProperty("user");
101
+ });
102
+
103
+ test("falls back to conversationId when there is no selectionSeed", async () => {
104
+ const { provider, calls } = fireworksPipeline();
105
+
106
+ await send(provider, { conversationId: "conv-xyz" });
107
+
108
+ expect(calls[0]!.headers?.[FIREWORKS_SESSION_AFFINITY_HEADER]).toBe(
109
+ "conv-xyz",
110
+ );
111
+ expect(JSON.stringify(calls[0]!.params)).not.toContain("conv-xyz");
112
+ });
113
+
114
+ test("uses the same key on every call of a conversation", async () => {
115
+ const { provider, calls } = fireworksPipeline();
116
+
117
+ await send(provider, { selectionSeed: "conv-xyz" });
118
+ await send(provider, { selectionSeed: "conv-xyz" });
119
+
120
+ expect(
121
+ calls.map((call) => call.headers?.[FIREWORKS_SESSION_AFFINITY_HEADER]),
122
+ ).toEqual(["conv-xyz", "conv-xyz"]);
123
+ });
124
+
125
+ test("sends no header when the call has no conversation key", async () => {
126
+ const { provider, calls } = fireworksPipeline();
127
+
128
+ await send(provider, {});
129
+ await send(provider, { selectionSeed: " ", conversationId: "" });
130
+
131
+ for (const call of calls) {
132
+ expect(call.headers ?? {}).not.toHaveProperty(
133
+ FIREWORKS_SESSION_AFFINITY_HEADER,
134
+ );
135
+ }
136
+ });
137
+
138
+ test("ignores caller-set request headers", async () => {
139
+ const { provider, calls } = fireworksPipeline();
140
+
141
+ await send(provider, {
142
+ requestHeaders: { [FIREWORKS_SESSION_AFFINITY_HEADER]: "shared-key" },
143
+ });
144
+
145
+ expect(calls[0]!.headers ?? {}).not.toHaveProperty(
146
+ FIREWORKS_SESSION_AFFINITY_HEADER,
147
+ );
148
+ });
149
+
150
+ test("does not send the header for other chat-completions providers", async () => {
151
+ const inner = new OpenAIChatCompletionsProvider("sk-test", "some-model", {
152
+ providerName: "together",
153
+ providerLabel: "Together",
154
+ });
155
+ const calls = stubChatCompletions(inner);
156
+ const provider = new RetryProvider(inner);
157
+
158
+ await send(provider, {
159
+ selectionSeed: "conv-xyz",
160
+ conversationId: "conv-xyz",
161
+ });
162
+
163
+ expect(calls[0]!.headers ?? {}).not.toHaveProperty(
164
+ FIREWORKS_SESSION_AFFINITY_HEADER,
165
+ );
166
+ expect(JSON.stringify(calls[0]!.params)).not.toContain("conv-xyz");
167
+ });
168
+ });
@@ -12,6 +12,30 @@ export interface FireworksProviderOptions {
12
12
 
13
13
  const DEFAULT_FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
14
14
 
15
+ /**
16
+ * Fireworks routing hint. Prompt caching works only within one replica, so
17
+ * serverless and multi-replica deployments use this header to send requests
18
+ * that share a key to the same replica.
19
+ * https://docs.fireworks.ai/guides/prompt-caching#optimizing-inference-request-for-caching
20
+ */
21
+ export const FIREWORKS_SESSION_AFFINITY_HEADER = "x-session-affinity";
22
+
23
+ /**
24
+ * Headers for one outgoing Fireworks request. `sessionKey` is the durable
25
+ * per-conversation key; without one the header is omitted, because a shared
26
+ * key would pin all keyless traffic to a single replica. Transport metadata
27
+ * only; nothing here enters the request body.
28
+ */
29
+ export function resolveFireworksRequestHeaders(
30
+ sessionKey: string | undefined,
31
+ ): Record<string, string> {
32
+ const key = sessionKey?.trim();
33
+ if (!key) {
34
+ return {};
35
+ }
36
+ return { [FIREWORKS_SESSION_AFFINITY_HEADER]: key };
37
+ }
38
+
15
39
  const FIREWORKS_MODEL_EFFORT_CEILINGS = modelEffortCeilings("fireworks");
16
40
  const FIREWORKS_MODEL_SUPPORTED_EFFORTS = modelSupportedEfforts("fireworks");
17
41
 
@@ -35,6 +35,7 @@ import {
35
35
  shouldSkipPrimary,
36
36
  tryAcquireRecoveryProbe,
37
37
  } from "./fallback-breaker.js";
38
+ import { resolveFireworksRequestHeaders } from "./fireworks/client.js";
38
39
  import { resolveLogitBiasPreset } from "./inference/logit-bias.js";
39
40
  import { MALFORMED_TOOL_CALL_MESSAGE } from "./malformed-tool-call.js";
40
41
  import {
@@ -681,6 +682,19 @@ function normalizeSendMessageOptions(
681
682
  nextConfig.requestHeaders = resolveOpenCodeRequestHeaders(conversationId);
682
683
  }
683
684
 
685
+ if (providerName === "fireworks") {
686
+ // Covers BYOK Fireworks and the Vellum-managed Fireworks upstream, which
687
+ // both dispatch through `FireworksProvider`. Keyless calls send no header.
688
+ const sessionKey = [config.selectionSeed, config.conversationId].find(
689
+ (value): value is string =>
690
+ typeof value === "string" && value.trim().length > 0,
691
+ );
692
+ const headers = resolveFireworksRequestHeaders(sessionKey);
693
+ if (Object.keys(headers).length > 0) {
694
+ nextConfig.requestHeaders = headers;
695
+ }
696
+ }
697
+
684
698
  // `overrideProfile`, `forceOverrideProfile`, `selectionSeed`,
685
699
  // `conversationId`, and `nativeWebSearchSentinel` are routing/resolution-time
686
700
  // concerns (consumed by the resolver below, `CallSiteRoutingProvider`'s
@@ -412,8 +412,9 @@ export interface SendMessageConfig {
412
412
  usageAttributionHeaders?: Record<string, string>;
413
413
  /**
414
414
  * Per-request HTTP headers merged onto the transport. `RetryProvider`
415
- * stamps these for providers that need support-lookup headers (OpenCode
416
- * `x-opencode-session` / `x-opencode-request`). Provider clients pass
415
+ * stamps these for providers that need per-conversation routing headers
416
+ * (OpenCode `x-opencode-session` / `x-opencode-request`, Fireworks
417
+ * `x-session-affinity`). Provider clients pass
417
418
  * them through SDK request options only and must never include this
418
419
  * object in provider JSON request bodies.
419
420
  */