Skip to content

Commit 59a13e9

Browse files
FIX: Allow environment and tier to be passed to providers with compiled prompts
1 parent d7b9aed commit 59a13e9

16 files changed

Lines changed: 598 additions & 65 deletions

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,29 @@ import { geminiAdapter } from 'promptopskit/gemini';
182182
import { openrouterAdapter } from 'promptopskit/openrouter';
183183
```
184184

185+
Direct adapter rendering also accepts `environment` and `tier` selectors. This is useful for compiled JSON/ESM assets in browser, edge, or worker code:
186+
187+
```typescript
188+
import type { ResolvedPromptAsset } from 'promptopskit';
189+
import { openaiAdapter } from 'promptopskit/openai';
190+
import compiledPrompt from './dist/prompts/summarizePullRequest.mjs';
191+
192+
const prompt = compiledPrompt as ResolvedPromptAsset;
193+
194+
const validation = openaiAdapter.validate(prompt, { environment: 'dev' });
195+
if (!validation.valid) {
196+
throw new Error(validation.errors.join(' '));
197+
}
198+
199+
const request = openaiAdapter.render(prompt, {
200+
environment: 'dev',
201+
variables: {
202+
pull_request_body: 'Implement theming and dark mode across the app.',
203+
},
204+
strict: true,
205+
});
206+
```
207+
185208
## Optional UsageTap Tracking
186209

187210
PromptOpsKit can also help you track provider calls with UsageTap.com while keeping the core render API body-only.

SKILL.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,55 @@ const kit = createPromptOpsKit({
281281
});
282282
```
283283

284+
### Browser / client-side demos
285+
286+
For browser code, client components, or frontend-only demos:
287+
288+
- Do not import `createPromptOpsKit`, `loadPromptFile`, or other top-level runtime helpers from `promptopskit` in client code. The top-level entry loads Node file-system/path modules for source and compiled prompt loading.
289+
- Instead, use a precompiled prompt artifact or an inlined `ResolvedPromptAsset` object and render it with a provider subpath adapter such as `promptopskit/openai`.
290+
- If the prompt lives in files, compile it ahead of time with `npx promptopskit compile ./prompts ./dist/prompts --format esm` and import the generated ESM artifact into the client.
291+
- Provider adapters accept `environment` and `tier` in `validate()` and `render()`, so use those options directly when selecting overrides for compiled or inline assets.
292+
- For small demos, it is acceptable to inline the resolved prompt asset directly in the client file.
293+
- Keep transport and auth in the application layer. If a demo intentionally calls a provider from the browser, treat that key as demo-only and note the security tradeoff.
294+
295+
Example:
296+
297+
```typescript
298+
import type { ResolvedPromptAsset } from 'promptopskit';
299+
import { openaiAdapter } from 'promptopskit/openai';
300+
301+
const prompt: ResolvedPromptAsset = {
302+
id: 'summarizePullRequest',
303+
schema_version: 1,
304+
provider: 'openai',
305+
model: 'gpt-5.4',
306+
context: {
307+
inputs: [{ name: 'pull_request_body', max_size: 8000 }],
308+
},
309+
sections: {
310+
system_instructions: 'You summarize pull requests clearly and concisely.',
311+
prompt_template: 'Summarize this pull request:\n\n{{ pull_request_body }}',
312+
},
313+
};
314+
315+
const validation = openaiAdapter.validate(prompt, {
316+
environment: 'prod',
317+
});
318+
if (!validation.valid) {
319+
throw new Error(validation.errors.join(' '));
320+
}
321+
322+
const request = openaiAdapter.render(prompt, {
323+
environment: 'prod',
324+
variables: {
325+
pull_request_body: 'Add theming and dark mode support to the application.',
326+
},
327+
strict: true,
328+
});
329+
330+
// request.body is ready for the OpenAI SDK or fetch.
331+
```
332+
284333
### Step-by-step API
285334

286335
```typescript

docs/api-reference.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,16 @@ Get a provider adapter by name.
226226

227227
```typescript
228228
const adapter = getAdapter('openai');
229-
const validation = adapter.validate(resolvedAsset);
230-
const request = adapter.render(resolvedAsset, { variables: { name: 'World' } });
229+
const validation = adapter.validate(resolvedAsset, { environment: 'dev' });
230+
const request = adapter.render(resolvedAsset, {
231+
environment: 'dev',
232+
tier: 'pro',
233+
variables: { name: 'World' },
234+
});
231235
```
232236

237+
`RuntimeRenderOptions` for direct adapter rendering supports `environment`, `tier`, `runtime`, `variables`, `history`, `toolRegistry`, and `strict`.
238+
233239
## Standalone `renderPrompt`
234240

235241
A convenience wrapper that creates a temporary `PromptOpsKit` instance:

docs/providers.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,63 @@ Each adapter implements the `ProviderAdapter` interface:
4545
```typescript
4646
interface ProviderAdapter {
4747
name: string;
48-
validate(asset: ResolvedPromptAsset): ValidationResult;
48+
validate(asset: ResolvedPromptAsset, runtime?: RuntimeRenderOptions): ValidationResult;
4949
render(asset: ResolvedPromptAsset, runtime: RuntimeRenderOptions): ProviderRequest;
5050
}
5151
```
5252

53+
Direct adapter rendering accepts the same `environment` and `tier` selectors as `kit.renderPrompt()`. This is especially useful with compiled JSON/ESM assets in browser, edge, or worker code.
54+
55+
## Browser / client-side usage
56+
57+
The top-level `promptopskit` runtime is Node-oriented. It supports prompt loading and compilation flows that import file-system/path modules, so do not use `createPromptOpsKit()` inside browser-only code or client components.
58+
59+
For frontend demos or browser rendering:
60+
61+
- Precompile prompts to ESM with `promptopskit compile ./prompts ./dist/prompts --format esm` and import the generated artifact, or
62+
- Inline a `ResolvedPromptAsset` object directly in the client bundle for a small demo.
63+
- Pass `environment` and `tier` directly to `adapter.validate()` and `adapter.render()` when you need overrides on the client side.
64+
65+
Then render with a provider subpath adapter:
66+
67+
```typescript
68+
import type { ResolvedPromptAsset } from 'promptopskit';
69+
import { openaiAdapter } from 'promptopskit/openai';
70+
71+
const prompt: ResolvedPromptAsset = {
72+
id: 'summarizePullRequest',
73+
schema_version: 1,
74+
provider: 'openai',
75+
model: 'gpt-5.4',
76+
context: {
77+
inputs: [{ name: 'pull_request_body', max_size: 8000 }],
78+
},
79+
sections: {
80+
system_instructions: 'You summarize pull requests clearly and concisely.',
81+
prompt_template: 'Summarize this pull request:\n\n{{ pull_request_body }}',
82+
},
83+
};
84+
85+
const validation = openaiAdapter.validate(prompt, {
86+
environment: 'prod',
87+
});
88+
if (!validation.valid) {
89+
throw new Error(validation.errors.join(' '));
90+
}
91+
92+
const { body } = openaiAdapter.render(prompt, {
93+
environment: 'prod',
94+
variables: {
95+
pull_request_body: 'Implement theming and dark mode across the app.',
96+
},
97+
strict: true,
98+
});
99+
100+
// Send `body` with the OpenAI SDK or fetch.
101+
```
102+
103+
This pattern keeps PromptOpsKit responsible for prompt rendering while leaving HTTP transport, auth, and browser-specific safety decisions in the app.
104+
53105
## Optional UsageTap tracking
54106

55107
If you want UsageTap begin/end tracking around a provider call, use the optional `promptopskit/usagetap` helper layer.
@@ -220,6 +272,9 @@ Each adapter validates the asset before rendering. Common checks:
220272

221273
```typescript
222274
const adapter = getAdapter('openai');
223-
const validation = adapter.validate(resolvedAsset);
275+
const validation = adapter.validate(resolvedAsset, {
276+
environment: 'dev',
277+
tier: 'pro',
278+
});
224279
// { valid: boolean, errors: string[], warnings: string[] }
225280
```

src/cli/commands/defaults-root.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { existsSync } from 'node:fs';
2+
import { dirname, join, resolve } from 'node:path';
3+
4+
export function findDefaultsRoot(filePath: string): string {
5+
let current = resolve(dirname(filePath));
6+
let root = current;
7+
let foundDefaults = false;
8+
9+
while (true) {
10+
if (existsSync(join(current, 'defaults.md'))) {
11+
root = current;
12+
foundDefaults = true;
13+
} else if (foundDefaults) {
14+
return root;
15+
}
16+
17+
const parent = dirname(current);
18+
if (parent === current) {
19+
return root;
20+
}
21+
22+
current = parent;
23+
}
24+
}

src/cli/commands/inspect.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { loadPromptFile } from '../../parser/index.js';
22
import { resolveIncludes } from '../../composition/index.js';
3+
import { findDefaultsRoot } from './defaults-root.js';
34

45
const HELP = `
56
promptopskit inspect <file>
@@ -22,7 +23,8 @@ export async function inspect(args: string[]): Promise<void> {
2223
process.exit(1);
2324
}
2425

25-
const { asset: parsed } = await loadPromptFile(file);
26+
const defaultsRoot = findDefaultsRoot(file);
27+
const { asset: parsed } = await loadPromptFile(file, { defaultsRoot });
2628

2729
// Resolve includes so the output shows the fully resolved asset
2830
const asset = (parsed.includes && parsed.includes.length > 0)

src/cli/commands/render.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { loadPromptFile } from '../../parser/index.js';
44
import { resolveIncludes } from '../../composition/index.js';
55
import { applyOverrides } from '../../overrides/index.js';
66
import { interpolate } from '../../renderer/interpolate.js';
7+
import { findDefaultsRoot } from './defaults-root.js';
78

89
const HELP = `
910
promptopskit render <file> [options]
@@ -56,7 +57,8 @@ export async function render(args: string[]): Promise<void> {
5657
}
5758
}
5859

59-
const { asset: parsed } = await loadPromptFile(file);
60+
const defaultsRoot = findDefaultsRoot(file);
61+
const { asset: parsed } = await loadPromptFile(file, { defaultsRoot });
6062

6163
// Resolve includes (matching the library pipeline)
6264
const resolved = (parsed.includes && parsed.includes.length > 0)

src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getAdapter } from './providers/index.js';
1010
import { validateAsset } from './validation/index.js';
1111
import { PromptCache } from './cache.js';
1212
import { collectContextSizeWarnings } from './context.js';
13-
import type { PromptAsset, ResolvedPromptAsset } from './schema/index.js';
13+
import type { PromptAsset, PromptAssetOverrides, ResolvedPromptAsset } from './schema/index.js';
1414
import type { ProviderRequest, RuntimeRenderOptions } from './providers/types.js';
1515
import type { PromptValidationResult } from './validation/index.js';
1616

@@ -94,6 +94,8 @@ export interface RenderPromptOptions {
9494
environment?: string;
9595
/** Tier override */
9696
tier?: string;
97+
/** Runtime overrides applied after environment and tier */
98+
runtime?: Partial<PromptAssetOverrides>;
9799
/** Variables for interpolation */
98100
variables?: Record<string, string>;
99101
/** Conversation history */
@@ -236,7 +238,7 @@ export class PromptOpsKit {
236238
*/
237239
async resolvePrompt(
238240
promptPath: string,
239-
options: { environment?: string; tier?: string } = {},
241+
options: { environment?: string; tier?: string; runtime?: Partial<PromptAssetOverrides> } = {},
240242
): Promise<ResolvedPromptAsset> {
241243
let asset = await this.loadPrompt(promptPath);
242244

@@ -250,6 +252,7 @@ export class PromptOpsKit {
250252
asset = applyOverrides(asset, {
251253
environment: options.environment,
252254
tier: options.tier,
255+
runtime: options.runtime,
253256
});
254257

255258
return asset as ResolvedPromptAsset;
@@ -267,12 +270,14 @@ export class PromptOpsKit {
267270
const overridden = applyOverrides(asset, {
268271
environment: options.environment,
269272
tier: options.tier,
273+
runtime: options.runtime,
270274
});
271275
resolved = overridden as ResolvedPromptAsset;
272276
} else if (options.path) {
273277
resolved = await this.resolvePrompt(options.path, {
274278
environment: options.environment,
275279
tier: options.tier,
280+
runtime: options.runtime,
276281
});
277282
} else {
278283
throw new Error('Either "path" or "source" must be provided to renderPrompt()');

src/providers/anthropic.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
RuntimeRenderOptions,
77
} from './types.js';
88
import { renderSections } from '../renderer/index.js';
9+
import { resolveAssetForProvider } from './resolve-asset.js';
910

1011
/**
1112
* Anthropic provider adapter.
@@ -14,29 +15,31 @@ import { renderSections } from '../renderer/index.js';
1415
export const anthropicAdapter: ProviderAdapter = {
1516
name: 'anthropic',
1617

17-
validate(asset: ResolvedPromptAsset): ValidationResult {
18+
validate(asset: ResolvedPromptAsset, runtime?: RuntimeRenderOptions): ValidationResult {
19+
const resolvedAsset = resolveAssetForProvider(asset, runtime);
1820
const errors: string[] = [];
1921
const warnings: string[] = [];
2022

21-
if (!asset.model) {
23+
if (!resolvedAsset.model) {
2224
errors.push('Anthropic adapter requires a model to be specified.');
2325
}
2426

25-
if (asset.sampling?.frequency_penalty !== undefined) {
27+
if (resolvedAsset.sampling?.frequency_penalty !== undefined) {
2628
warnings.push('Anthropic does not support frequency_penalty. It will be ignored.');
2729
}
28-
if (asset.sampling?.presence_penalty !== undefined) {
30+
if (resolvedAsset.sampling?.presence_penalty !== undefined) {
2931
warnings.push('Anthropic does not support presence_penalty. It will be ignored.');
3032
}
31-
if (asset.reasoning?.effort !== undefined) {
33+
if (resolvedAsset.reasoning?.effort !== undefined) {
3234
warnings.push('Anthropic uses budget_tokens for thinking, not effort. effort will be mapped approximately.');
3335
}
3436

3537
return { valid: errors.length === 0, errors, warnings };
3638
},
3739

3840
render(asset: ResolvedPromptAsset, runtime: RuntimeRenderOptions): ProviderRequest {
39-
const sections = renderSections(asset, {
41+
const resolvedAsset = resolveAssetForProvider(asset, runtime);
42+
const sections = renderSections(resolvedAsset, {
4043
variables: runtime.variables,
4144
strict: runtime.strict,
4245
});
@@ -56,7 +59,7 @@ export const anthropicAdapter: ProviderAdapter = {
5659
}
5760

5861
const body: Record<string, unknown> = {
59-
model: asset.model,
62+
model: resolvedAsset.model,
6063
messages,
6164
};
6265

@@ -66,32 +69,32 @@ export const anthropicAdapter: ProviderAdapter = {
6669
}
6770

6871
// Sampling params
69-
if (asset.sampling?.temperature !== undefined) body.temperature = asset.sampling.temperature;
70-
if (asset.sampling?.top_p !== undefined) body.top_p = asset.sampling.top_p;
71-
if (asset.sampling?.stop !== undefined) body.stop_sequences = asset.sampling.stop;
72-
if (asset.sampling?.max_output_tokens !== undefined) {
73-
body.max_tokens = asset.sampling.max_output_tokens;
72+
if (resolvedAsset.sampling?.temperature !== undefined) body.temperature = resolvedAsset.sampling.temperature;
73+
if (resolvedAsset.sampling?.top_p !== undefined) body.top_p = resolvedAsset.sampling.top_p;
74+
if (resolvedAsset.sampling?.stop !== undefined) body.stop_sequences = resolvedAsset.sampling.stop;
75+
if (resolvedAsset.sampling?.max_output_tokens !== undefined) {
76+
body.max_tokens = resolvedAsset.sampling.max_output_tokens;
7477
} else {
7578
// Anthropic requires max_tokens
7679
body.max_tokens = 4096;
7780
}
7881

7982
// Thinking/reasoning
80-
if (asset.reasoning?.budget_tokens) {
83+
if (resolvedAsset.reasoning?.budget_tokens) {
8184
body.thinking = {
8285
type: 'enabled',
83-
budget_tokens: asset.reasoning.budget_tokens,
86+
budget_tokens: resolvedAsset.reasoning.budget_tokens,
8487
};
8588
}
8689

8790
// Streaming
88-
if (asset.response?.stream !== undefined) {
89-
body.stream = asset.response.stream;
91+
if (resolvedAsset.response?.stream !== undefined) {
92+
body.stream = resolvedAsset.response.stream;
9093
}
9194

9295
// Tools
93-
if (asset.tools && asset.tools.length > 0) {
94-
body.tools = asset.tools.map((tool) => {
96+
if (resolvedAsset.tools && resolvedAsset.tools.length > 0) {
97+
body.tools = resolvedAsset.tools.map((tool) => {
9598
if (typeof tool === 'string') {
9699
const def = runtime.toolRegistry?.[tool];
97100
if (def) return def;
@@ -108,7 +111,7 @@ export const anthropicAdapter: ProviderAdapter = {
108111
return {
109112
body,
110113
provider: 'anthropic',
111-
model: asset.model ?? 'unknown',
114+
model: resolvedAsset.model ?? 'unknown',
112115
};
113116
},
114117
};

0 commit comments

Comments
 (0)