defiant-jazz 0.6.2 → 0.8.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 CHANGED
@@ -28,15 +28,21 @@ await refine("Hello world", "dylan");
28
28
  ```
29
29
 
30
30
  Set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in your environment and you're done —
31
- whichever is present is used automatically (Anthropic first if both are set).
31
+ whichever one is present is used. If both are set, the library won't pick for
32
+ you; pass `provider` instead (see [Choosing a provider](#choosing-a-provider)).
32
33
 
33
34
  **Default models: `claude-sonnet-5` (Anthropic), `gpt-4o-mini` (OpenAI).**
34
35
  Sonnet 5 was chosen over Opus 5 after a head-to-head comparison found
35
36
  matching output quality at 40% of the cost — see
36
37
  [model-comparison.md](model-comparison.md) for the full writeup, including a
37
38
  documented regression on Haiku 4.5 that ruled it out as a default. Override
38
- with `model` on `createDefiantJazz()` or per call; see
39
- [Choosing a provider](#choosing-a-provider).
39
+ with `model` on `createDefiantJazz()` or per call. The defaults are also
40
+ exported, so you can check what a zero-config call will run:
41
+
42
+ ```typescript
43
+ import { DEFAULT_MODELS } from 'defiant-jazz';
44
+ // { anthropic: 'claude-sonnet-5', openai: 'gpt-4o-mini' }
45
+ ```
40
46
 
41
47
  ## Lore
42
48
 
@@ -91,7 +97,28 @@ await claude.dylan("Hello world");
91
97
  await gpt.dylan("Hello world");
92
98
  ```
93
99
 
94
- `refine` is a shared default instance using auto-detected credentials. Use
100
+ How the provider is chosen:
101
+
102
+ - `provider` set → that provider. Its key comes from `apiKey`, or from
103
+ `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` if `apiKey` is omitted.
104
+ - `provider` omitted, exactly one env key set → that provider.
105
+ - `provider` omitted, both env keys set → throws. The library never decides
106
+ whose account gets billed.
107
+ - `apiKey` passed without `provider` → throws, so a key is never sent to the
108
+ wrong service.
109
+
110
+ Only those two env var names are read. To use a key stored under a different
111
+ name, read it yourself and pass it as `apiKey`:
112
+
113
+ ```typescript
114
+ const gpt = createDefiantJazz({
115
+ provider: 'openai',
116
+ apiKey: process.env.MY_TEAM_OPENAI_KEY,
117
+ });
118
+ ```
119
+
120
+ `refine` is a shared default instance that follows the same rules; point it at
121
+ a provider with `refine.configure({ provider: 'openai' })`. Use
95
122
  `createDefiantJazz()` whenever you need an isolated instance — a specific
96
123
  provider, different API keys per request, a non-default model, testing.
97
124
 
@@ -101,6 +128,42 @@ Per-call overrides work on both forms:
101
128
  await refine.dylan("Hello world", { model: 'claude-opus-5', maxTokens: 1024 });
102
129
  ```
103
130
 
131
+ ### Effort (Anthropic only)
132
+
133
+ `effort` controls how much the model reasons before answering: `'low'`,
134
+ `'medium'`, `'high'`, `'xhigh'` or `'max'`. Lower is cheaper and faster. Leave
135
+ it unset to use the model's own default, which varies by model.
136
+
137
+ ```typescript
138
+ const fast = createDefiantJazz({ effort: 'low' });
139
+ await refine.dylan("Hello world", { effort: 'low' }); // per-call works too
140
+ ```
141
+
142
+ - Setting `effort` with the OpenAI provider throws.
143
+ - Not every model accepts every level (older models may reject `effort`
144
+ entirely); the API's error is passed through.
145
+ - Reasoning tokens count toward `maxTokens`. At `xhigh` or `max`, raise
146
+ `maxTokens` if you see "returned no text content (stop_reason: max_tokens)".
147
+
148
+ ## Usage and cost
149
+
150
+ Every result includes the model that answered and the request's token
151
+ counts, normalized across providers:
152
+
153
+ ```typescript
154
+ const result = await refine.dylan("Hello world");
155
+ // {
156
+ // text: "...",
157
+ // character: "Dylan G.",
158
+ // model: "claude-sonnet-5",
159
+ // usage: { inputTokens: 2770, outputTokens: 77 },
160
+ // }
161
+ ```
162
+
163
+ `outputTokens` includes any reasoning tokens the model spent before answering
164
+ (see [Effort](#effort-anthropic-only)). `usage` is omitted if the provider
165
+ doesn't report it.
166
+
104
167
  ## Characters
105
168
 
106
169
  ```typescript
package/dist/client.js CHANGED
@@ -12,14 +12,23 @@ function resolveProvider(options) {
12
12
  }
13
13
  return { provider: options.provider, apiKey };
14
14
  }
15
+ // No explicit provider. Only infer one when there's exactly one candidate;
16
+ // never guess between two, since that picks whose account gets billed.
15
17
  if (options.apiKey) {
16
- return { provider: "anthropic", apiKey: options.apiKey };
18
+ throw new Error("apiKey requires provider so the key isn't sent to the wrong service. " +
19
+ "Pass { provider: 'anthropic' } or { provider: 'openai' } alongside it.");
17
20
  }
18
- if (process.env.ANTHROPIC_API_KEY) {
19
- return { provider: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY };
21
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
22
+ const openaiKey = process.env.OPENAI_API_KEY;
23
+ if (anthropicKey && openaiKey) {
24
+ throw new Error("Both ANTHROPIC_API_KEY and OPENAI_API_KEY are set. " +
25
+ "Pass { provider: 'anthropic' } or { provider: 'openai' } to choose.");
20
26
  }
21
- if (process.env.OPENAI_API_KEY) {
22
- return { provider: "openai", apiKey: process.env.OPENAI_API_KEY };
27
+ if (anthropicKey) {
28
+ return { provider: "anthropic", apiKey: anthropicKey };
29
+ }
30
+ if (openaiKey) {
31
+ return { provider: "openai", apiKey: openaiKey };
23
32
  }
24
33
  throw new Error("No API key found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY " +
25
34
  "(or pass apiKey/provider to configure()/createDefiantJazz()).");
@@ -51,13 +60,17 @@ export function createDefiantJazz(initialOptions = {}) {
51
60
  }
52
61
  const characterName = CHARACTERS[characterKey];
53
62
  const { provider, client } = getClient();
54
- const defaults = { model: config.model, maxTokens: config.maxTokens };
63
+ const effort = options?.effort ?? config.effort;
64
+ if (effort !== undefined && provider === "openai") {
65
+ throw new Error("effort is only supported with the Anthropic provider.");
66
+ }
67
+ const defaults = { model: config.model, maxTokens: config.maxTokens, effort: config.effort };
55
68
  const lore = options?.lore ?? config.lore ?? true;
56
69
  const systemPrompt = buildSystemPrompt(lore);
57
- const resultText = provider === "anthropic"
70
+ const result = provider === "anthropic"
58
71
  ? await runAnthropic(client, characterName, text, systemPrompt, defaults, options)
59
72
  : await runOpenAi(client, characterName, text, systemPrompt, defaults, options);
60
- return { text: resultText, character: characterName };
73
+ return { ...result, character: characterName };
61
74
  }
62
75
  const fn = ((text, character, options) => transform(text, character, options));
63
76
  fn.mark = (text, options) => transform(text, "mark", options);
@@ -0,0 +1,7 @@
1
+ import type { Provider } from "./types.js";
2
+ /**
3
+ * The model each provider uses when `model` is not set. Exported so callers
4
+ * can see what a zero-config call will run (and be billed for) without
5
+ * reading the docs. See model-comparison.md for why Sonnet 5 is the default.
6
+ */
7
+ export declare const DEFAULT_MODELS: Readonly<Record<Provider, string>>;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The model each provider uses when `model` is not set. Exported so callers
3
+ * can see what a zero-config call will run (and be billed for) without
4
+ * reading the docs. See model-comparison.md for why Sonnet 5 is the default.
5
+ */
6
+ export const DEFAULT_MODELS = Object.freeze({
7
+ anthropic: "claude-sonnet-5",
8
+ openai: "gpt-4o-mini",
9
+ });
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createDefiantJazz, type DefiantJazz } from "./client.js";
2
+ import { DEFAULT_MODELS } from "./defaults.js";
2
3
  import { CHARACTERS } from "./prompt.js";
3
- import type { CharacterKey, DefiantJazzOptions, RefineOptions, RefineResult } from "./types.js";
4
- export type { CharacterKey, DefiantJazzOptions, RefineOptions, RefineResult };
5
- export { CHARACTERS, createDefiantJazz };
4
+ import type { CharacterKey, DefiantJazzOptions, Effort, RefineOptions, RefineResult, TokenUsage } from "./types.js";
5
+ export type { CharacterKey, DefiantJazzOptions, Effort, RefineOptions, RefineResult, TokenUsage, };
6
+ export { CHARACTERS, DEFAULT_MODELS, createDefiantJazz };
6
7
  /**
7
8
  * Configure the default `refine` instance with an API key and optional
8
9
  * model/provider overrides. Prefer `createDefiantJazz()` for an isolated
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createDefiantJazz } from "./client.js";
2
+ import { DEFAULT_MODELS } from "./defaults.js";
2
3
  import { CHARACTERS } from "./prompt.js";
3
- export { CHARACTERS, createDefiantJazz };
4
+ export { CHARACTERS, DEFAULT_MODELS, createDefiantJazz };
4
5
  let defaultInstance = null;
5
6
  function getDefaultInstance() {
6
7
  if (!defaultInstance) {
@@ -1,7 +1,8 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
- import type { RefineOptions } from "../types.js";
2
+ import type { Effort, RefineOptions, RefineResult } from "../types.js";
3
3
  export declare function createAnthropicClient(apiKey: string): Anthropic;
4
4
  export declare function runAnthropic(client: Anthropic, characterName: string, text: string, systemPrompt: string, defaults: {
5
5
  model?: string;
6
6
  maxTokens?: number;
7
- }, options?: RefineOptions): Promise<string>;
7
+ effort?: Effort;
8
+ }, options?: RefineOptions): Promise<Omit<RefineResult, "character">>;
@@ -1,14 +1,17 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
- const DEFAULT_MODEL = "claude-sonnet-5";
2
+ import { DEFAULT_MODELS } from "../defaults.js";
3
+ const DEFAULT_MODEL = DEFAULT_MODELS.anthropic;
3
4
  const DEFAULT_MAX_TOKENS = 4096;
4
5
  export function createAnthropicClient(apiKey) {
5
6
  return new Anthropic({ apiKey });
6
7
  }
7
8
  export async function runAnthropic(client, characterName, text, systemPrompt, defaults, options) {
9
+ const effort = options?.effort ?? defaults.effort;
8
10
  try {
9
11
  const response = await client.messages.create({
10
12
  model: options?.model ?? defaults.model ?? DEFAULT_MODEL,
11
13
  max_tokens: options?.maxTokens ?? defaults.maxTokens ?? DEFAULT_MAX_TOKENS,
14
+ ...(effort !== undefined && { output_config: { effort } }),
12
15
  system: systemPrompt,
13
16
  messages: [
14
17
  {
@@ -21,7 +24,14 @@ export async function runAnthropic(client, characterName, text, systemPrompt, de
21
24
  if (!textBlock) {
22
25
  throw new Error(`Anthropic returned no text content (stop_reason: ${response.stop_reason})`);
23
26
  }
24
- return textBlock.text;
27
+ return {
28
+ text: textBlock.text,
29
+ model: response.model,
30
+ usage: response.usage && {
31
+ inputTokens: response.usage.input_tokens,
32
+ outputTokens: response.usage.output_tokens,
33
+ },
34
+ };
25
35
  }
26
36
  catch (error) {
27
37
  if (error instanceof Anthropic.AuthenticationError) {
@@ -1,7 +1,7 @@
1
1
  import OpenAI from "openai";
2
- import type { RefineOptions } from "../types.js";
2
+ import type { RefineOptions, RefineResult } from "../types.js";
3
3
  export declare function createOpenAiClient(apiKey: string): OpenAI;
4
4
  export declare function runOpenAi(client: OpenAI, characterName: string, text: string, systemPrompt: string, defaults: {
5
5
  model?: string;
6
6
  maxTokens?: number;
7
- }, options?: RefineOptions): Promise<string>;
7
+ }, options?: RefineOptions): Promise<Omit<RefineResult, "character">>;
@@ -1,5 +1,6 @@
1
1
  import OpenAI from "openai";
2
- const DEFAULT_MODEL = "gpt-4o-mini";
2
+ import { DEFAULT_MODELS } from "../defaults.js";
3
+ const DEFAULT_MODEL = DEFAULT_MODELS.openai;
3
4
  const DEFAULT_MAX_TOKENS = 4096;
4
5
  export function createOpenAiClient(apiKey) {
5
6
  return new OpenAI({ apiKey });
@@ -20,5 +21,12 @@ export async function runOpenAi(client, characterName, text, systemPrompt, defau
20
21
  if (!content) {
21
22
  throw new Error("OpenAI returned no content.");
22
23
  }
23
- return content;
24
+ return {
25
+ text: content,
26
+ model: completion.model,
27
+ usage: completion.usage && {
28
+ inputTokens: completion.usage.prompt_tokens,
29
+ outputTokens: completion.usage.completion_tokens,
30
+ },
31
+ };
24
32
  }
package/dist/types.d.ts CHANGED
@@ -1,15 +1,37 @@
1
+ import type Anthropic from "@anthropic-ai/sdk";
1
2
  export type CharacterKey = "mark" | "irving" | "dylan" | "milchick";
2
3
  export type Provider = "anthropic" | "openai";
4
+ /** Anthropic effort level, taken from the SDK so new levels arrive with SDK updates. */
5
+ export type Effort = NonNullable<Anthropic.OutputConfig["effort"]>;
6
+ export interface TokenUsage {
7
+ inputTokens: number;
8
+ /** Includes any reasoning tokens the model spent before answering. */
9
+ outputTokens: number;
10
+ }
3
11
  export interface RefineResult {
4
12
  text: string;
5
13
  character: string;
14
+ /** The model that answered, as reported by the provider. */
15
+ model: string;
16
+ /** Token counts for this request, when the provider reports them. */
17
+ usage?: TokenUsage;
6
18
  }
7
19
  export interface DefiantJazzOptions {
8
- /** Explicit provider. Defaults to whichever API key is set (Anthropic first). */
20
+ /**
21
+ * Which API to call. Optional only when exactly one of `ANTHROPIC_API_KEY`
22
+ * or `OPENAI_API_KEY` is set and `apiKey` is not passed; when the choice
23
+ * would be ambiguous, the library throws rather than guess.
24
+ */
9
25
  provider?: Provider;
10
26
  apiKey?: string;
11
27
  model?: string;
12
28
  maxTokens?: number;
29
+ /**
30
+ * How much the model reasons before answering (Anthropic only). Lower is
31
+ * cheaper and faster. Leave unset to use the model's own default, which
32
+ * varies by model. Setting it with the OpenAI provider throws.
33
+ */
34
+ effort?: Effort;
13
35
  /**
14
36
  * Whether characters reach for Lumon/Severance vocabulary (the break room,
15
37
  * waffle parties, Kier, innie/outie, etc.) when a quote's situation calls
@@ -21,6 +43,11 @@ export interface DefiantJazzOptions {
21
43
  export interface RefineOptions {
22
44
  model?: string;
23
45
  maxTokens?: number;
46
+ /**
47
+ * How much the model reasons before answering (Anthropic only). Overrides
48
+ * the instance-level setting. Leave unset to use the model's own default.
49
+ */
50
+ effort?: Effort;
24
51
  signal?: AbortSignal;
25
52
  /**
26
53
  * Whether this call reaches for Lumon/Severance vocabulary. Overrides the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "defiant-jazz",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Transform quotes into Severance character voices using Claude or OpenAI",
5
5
  "license": "MIT",
6
6
  "author": "pelikenjones",