npm package: @genezio/signal
Description: Node.js SDK for the Signal LLM Infrastructure Platform
Author: Genezio
Latest version: 0.1.1

# @genezio/signal

Node.js SDK for the [Signal](https://signal.backend.genezio.ai) LLM Infrastructure Platform.

## Installation

```bash
npm install @genezio/signal
```

## Quick Start

```typescript
import { Signal } from '@genezio/signal';

const signal = new Signal({ apiKey: 'sk-sig-...' });

// Execute a prompt
const result = await signal.ask('prompt-id', { variables: { name: 'world' } });
console.log(result.answer);

// Stream a response
for await (const chunk of await signal.ask('prompt-id', { variables: { name: 'world' }, stream: true })) {
  if (chunk.type === 'delta') process.stdout.write(chunk.content ?? '');
  if (chunk.type === 'done') console.log('\nDone:', chunk.answer);
}
```

## Configuration

```typescript
const signal = new Signal({
  apiKey: 'sk-sig-...',              // Required — your Signal API key
  baseUrl: 'https://custom.url',     // Optional — defaults to https://signal.backend.genezio.ai
  cacheTtl: 10,                      // Optional — cache TTL in seconds (default: 10)
});
```

---

## API Reference

### `signal.ask(promptId, options?)`

Execute a prompt by ID. Returns the LLM response or a stream of chunks.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `promptId` | `string` | required | The prompt ID to execute. |
| `options.variables` | `Record<string, string>` | optional | Template variables (`{{name}}` placeholders). |
| `options.tag` | `string` | optional | Version tag (defaults to `"production"`). |
| `options.labels` | `string[]` | optional | Labels to attach to the interaction (max 50). |
| `options.stream` | `boolean` | optional | Enable streaming (default: `false`). |
| `options.mcpCredentials` | `Record<string, Record<string, string>>` | optional | Per-request MCP server header overrides. Keys are MCP server names (as configured in the Signal UI). Values are objects mapping header names to their values — these merge with/override the headers stored in the server definition. Example: `{ "github": { "Authorization": "Bearer ghp_..." } }` |

**Non-streaming:**

```typescript
const result = await signal.ask('prompt-id', {
  variables: { city: 'Paris' },
  tag: 'production',
  labels: ['user-query'],
});
// result: { answer: string, responseId: string | null }
```

**Streaming:**

```typescript
const stream = await signal.ask('prompt-id', {
  variables: { city: 'Paris' },
  stream: true,
});

for await (const chunk of stream) {
  switch (chunk.type) {
    case 'delta':      // chunk.content — text fragment
    case 'tool_call':  // chunk.toolCall — { server, tool, arguments }
    case 'tool_result':// chunk.toolResult — { server, tool, success, latencyMs }
    case 'done':       // chunk.answer, chunk.responseId, chunk.usage
    case 'error':      // chunk.error — error message
  }
}
```

---

### `signal.chat(options)`

Execute a raw chat completion without a prompt template.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `options.provider` | `string` | optional | LLM provider (e.g. `"openai"`, `"anthropic"`). Required unless `promptId` is specified (inherited from prompt). |
| `options.model` | `string` | optional | Model identifier (e.g. `"gpt-4o"`). Required unless `promptId` is specified (inherited from prompt). |
| `options.apiType` | `'chat' \| 'responses' \| 'messages'` | optional | API type. Required for providers with multiple types (e.g. OpenAI). Auto-detected when the provider supports only one. Inherited from prompt when `promptId` is specified. |
| `options.systemMessage` | `string` | optional | System message (max 100k chars). At least one of `systemMessage` or `userMessage` is required. |
| `options.userMessage` | `string` | optional | User message (max 100k chars). At least one of `systemMessage` or `userMessage` is required. |
| `options.temperature` | `number` | optional | Temperature (0–2). |
| `options.labels` | `string[]` | optional | Labels to attach. |
| `options.tags` | `string[]` | optional | Version tags. |
| `options.promptId` | `string` | optional | Associate with a prompt ID. |
| `options.previousResponseId` | `string` | optional | Previous response ID for multi-turn. |
| `options.conversationId` | `string` | optional | Conversation ID. |
| `options.webSearch` | `boolean` | optional | Enable web search (default: `false`). |
| `options.stream` | `boolean` | optional | Enable streaming (default: `false`). |
| `options.mcpServers` | `string[]` | optional | MCP server names to use. |

```typescript
// Non-streaming
const result = await signal.chat({
  provider: 'openai',
  model: 'gpt-4o',
  apiType: 'chat',
  systemMessage: 'You are a helpful assistant.',
  userMessage: 'What is 2+2?',
});
// result: { answer: string, responseId: string | null }

// Streaming
const stream = await signal.chat({
  provider: 'anthropic',
  model: 'claude-sonnet-4-20250514',
  systemMessage: 'You are a helpful assistant.',
  userMessage: 'Write a poem.',
  stream: true,
});
for await (const chunk of stream) {
  if (chunk.type === 'delta') process.stdout.write(chunk.content ?? '');
}
```

---

### Prompts

#### `signal.prompts.get(promptId, options?)`

Get a prompt's configuration. Defaults to the `"production"` tag if none specified.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `promptId` | `string` | required | The prompt ID. |
| `options.tag` | `string` | optional | Version tag (defaults to `"production"`). |

```typescript
const prompt = await signal.prompts.get('prompt-id');
// prompt: { promptId, promptName, provider, model, apiType, temperature,
//           webSearch, responseFormat, tags, systemTemplate, userTemplate,
//           mcpServers, toolChoice, maxToolRounds, cacheTtl }

// Same struct with a different tag
const staging = await signal.prompts.get('prompt-id', { tag: 'staging' });
```

#### `signal.prompts.resolve(promptId, options?)`

Resolve a prompt's template without executing it. Returns the same shape as `get()` but with `systemMessage`/`userMessage` (rendered) instead of `systemTemplate`/`userTemplate`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `options.variables` | `Record<string, string>` | optional | Template variables. |
| `options.tag` | `string` | optional | Version tag (defaults to `"production"`). |

```typescript
const resolved = await signal.prompts.resolve('prompt-id', {
  variables: { name: 'world' },
  tag: 'production',
});
// resolved: { promptId, promptName, provider, model, apiType, temperature,
//             webSearch, responseFormat, tags, systemMessage, userMessage,
//             cacheTtl }
```

---

### Prompt Versions

#### `signal.prompts.versions.create(promptId, options)`

Create a new version.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `options.provider` | `string` | optional | LLM provider. Inherited from latest version if omitted. |
| `options.model` | `string` | optional | Model. Inherited from latest version if omitted. |
| `options.apiType` | `'chat' \| 'responses' \| 'messages'` | optional | API type. Required for providers with multiple types (e.g. OpenAI). Auto-detected when the provider supports only one. |
| `options.systemTemplate` | `string` | optional | System template with `{{var}}` placeholders. |
| `options.userTemplate` | `string` | optional | User template. |
| `options.temperature` | `number` | optional | Temperature (0–2). Inherited from latest version if omitted. |
| `options.webSearch` | `boolean` | optional | Enable web search. Inherited from latest version if omitted. |
| `options.tags` | `string[]` | optional | Tags (auto-moved from other versions). |
| `options.mcpServers` | `string[]` | optional | MCP servers (max 20). Inherited from latest version if omitted. |
| `options.toolChoice` | `'auto' \| 'required' \| 'none'` | optional | Tool choice. Inherited from latest version if omitted. |
| `options.maxToolRounds` | `number` | optional | Max tool rounds (1–50). Inherited from latest version if omitted. |

```typescript
const version = await signal.prompts.versions.create('prompt-id', {
  provider: 'openai',
  model: 'gpt-4o',
  apiType: 'chat',
  systemTemplate: 'You are a helpful assistant for {{company}}.',
  userTemplate: '{{question}}',
  tags: ['production'],
});
```

---

### Interactions

#### `signal.interactions.log(options)`

Log an LLM interaction.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `options.conversationId` | `string` | optional | Conversation ID. |
| `options.interactionId` | `string` | optional | Custom ID (auto-generated if omitted). |
| `options.responseId` | `string` | optional | Response ID. |
| `options.previousResponseId` | `string` | optional | Previous response ID. |
| `options.provider` | `string` | required with model | LLM provider (e.g. `"openai"`). |
| `options.model` | `string` | optional | Model used. |
| `options.apiType` | `'chat' \| 'responses' \| 'messages'` | optional | API type used. |
| `options.messages` | `ChatMessage[]` | required | Conversation messages. |
| `options.output` | `string` | optional | Final output text. |
| `options.tokensInput` | `number` | optional | Input token count. |
| `options.tokensOutput` | `number` | optional | Output token count. |
| `options.latencyMs` | `number` | optional | Latency in milliseconds. |
| `options.promptId` | `string` | optional | Associated prompt ID. |
| `options.tags` | `string[]` | optional | Version tags (e.g. `["production"]`). |
| `options.labels` | `string[]` | optional | Labels (max 50). |
| `options.metadata` | `Record<string, unknown>` | optional | Custom metadata. |

```typescript
const result = await signal.interactions.log({
  messages: [
    { role: 'system', content: 'You are helpful.' },
    { role: 'user', content: 'Hi!' },
    { role: 'assistant', content: 'Hello!' },
  ],
  provider: 'openai',
  model: 'gpt-4o',
  tokensInput: 50,
  tokensOutput: 10,
});
// result: { success: true, interactionId: '...' }
```

#### `signal.interactions.appendMessages(interactionId, messages)`

Append messages to an existing interaction.

```typescript
await signal.interactions.appendMessages('interaction-id', [
  { role: 'user', content: 'Follow-up question' },
  { role: 'assistant', content: 'Follow-up answer' },
]);
```

#### `signal.interactions.setOutput(interactionId, options)`

Set or update the final output and token counts.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `options.output` | `string` | required | Final output text. |
| `options.tokensInput` | `number` | optional | Input tokens. |
| `options.tokensOutput` | `number` | optional | Output tokens. |
| `options.latencyMs` | `number` | optional | Latency in ms. |

```typescript
await signal.interactions.setOutput('interaction-id', {
  output: 'Final answer.',
  tokensInput: 100,
  tokensOutput: 25,
  latencyMs: 450,
});
```

---

### Feedback

#### `signal.feedback.submit(responseId, options)`

Submit or update feedback for a response.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `responseId` | `string` | required | The response ID. |
| `options.score` | `number` | required | Score (1–5). |
| `options.comment` | `string` | optional | Comment. |
| `options.amendedAnswer` | `string` | optional | Corrected answer. |

```typescript
await signal.feedback.submit('response-id', {
  score: 5,
  comment: 'Perfect answer!',
});
```

#### `signal.feedback.get(responseId)`

Get feedback for a response.

```typescript
const feedback = await signal.feedback.get('response-id');
// feedback: { responseId, score, comment, amendedAnswer, createdAt } | null
```

---

## Using Signal for Prompt Management Only

If you want to manage prompts in Signal but call the LLM yourself (e.g. with the OpenAI SDK directly), use `resolve()` to get the rendered messages and then log the interaction back for tracking and feedback.

```typescript
import { Signal } from '@genezio/signal';
import OpenAI from 'openai';

const signal = new Signal({ apiKey: 'sk-sig-...' });
const openai = new OpenAI({ apiKey: 'sk-...' });

// 1. Resolve the prompt — variables are filled in, config is returned
const resolved = await signal.prompts.resolve('prompt-id', {
  variables: { name: 'Alice', topic: 'weather' },
  tag: 'production',
});

// 2. Call the LLM yourself
const completion = await openai.chat.completions.create({
  model: resolved.model,
  temperature: resolved.temperature ?? undefined,
  messages: [
    { role: 'system', content: resolved.systemMessage },
    { role: 'user', content: resolved.userMessage },
  ],
});

const answer = completion.choices[0].message.content;

// 3. Log the interaction back to Signal for observability and feedback
await signal.interactions.log({
  messages: [
    { role: 'system', content: resolved.systemMessage },
    { role: 'user', content: resolved.userMessage },
    { role: 'assistant', content: answer },
  ],
  output: answer,
  provider: resolved.provider,
  model: resolved.model,
  promptId: resolved.promptId,
  tags: resolved.tags,
  tokensInput: completion.usage?.prompt_tokens,
  tokensOutput: completion.usage?.completion_tokens,
});
```

---

## Automatic Logging with `signal.wrap()`

Wrap an OpenAI or Anthropic SDK client to automatically log every LLM call to Signal — no manual `interactions.log()` needed.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `client` | `object` | required | An OpenAI or Anthropic SDK client instance. |
| `options.promptId` | `string` | optional | Associate logged interactions with a prompt. |
| `options.tags` | `string[]` | optional | Version tags. |
| `options.labels` | `string[]` | optional | Labels. |
| `options.metadata` | `object` | optional | Custom metadata. |
| `options.conversationId` | `string` | optional | Conversation ID. |

```typescript
import { Signal } from '@genezio/signal';
import OpenAI from 'openai';

const signal = new Signal({ apiKey: 'sk-sig-...' });
const openai = signal.wrap(new OpenAI({ apiKey: 'sk-...' }), {
  promptId: 'my-prompt',
  tags: ['production'],
});

// Works exactly like the normal OpenAI SDK — Signal logs it automatically
const completion = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: 'What is 2+2?' },
  ],
});
console.log(completion.choices[0].message.content);
// Signal automatically logs: messages, model, provider, tokens, latency
```

Supported clients: OpenAI (`chat.completions.create`) and Anthropic (`messages.create`). Streaming calls pass through without logging in v1.

---

## Function Tracing with `signal.trace()`

Wrap any async function to automatically log its inputs, output, and latency.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `fn` | `function` | required | The async function to wrap. |
| `options.promptId` | `string` | optional | Associate with a prompt. |
| `options.tags` | `string[]` | optional | Version tags. |
| `options.labels` | `string[]` | optional | Labels. |
| `options.metadata` | `object` | optional | Custom metadata. |

```typescript
const answerQuestion = signal.trace(async (question: string) => {
  const completion = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: question }],
  });
  return completion.choices[0].message.content;
}, { promptId: 'qa-prompt' });

const answer = await answerQuestion('What is the capital of France?');
// Signal logs: input args, output, latency, function name
```

---

## Cache

The SDK caches GET requests and prompt `resolve` calls in memory with a configurable TTL (default: 10 seconds). Streaming responses and write operations are never cached.

```typescript
// Invalidate cache for a specific prompt (resolve, get, versions)
signal.cache.invalidate('prompt-id');

// Clear the entire cache
signal.cache.clear();

```

---

## Error Handling

All errors extend `SignalError` with `status` and `code` properties.

| Error Class | Status | When |
|-------------|--------|------|
| `SignalValidationError` | 400 | Invalid request body or parameters. |
| `SignalAuthError` | 401 | Missing or invalid API key. |
| `SignalNotFoundError` | 404 | Resource not found. |
| `SignalRateLimitError` | 429 | Too many requests. |
| `SignalError` | other | Any other server error. |

```typescript
import { SignalNotFoundError } from '@genezio/signal';

try {
  await signal.prompts.get('nonexistent');
} catch (err) {
  if (err instanceof SignalNotFoundError) {
    console.log('Prompt not found');
  }
}
```

---

## Types

All TypeScript types are exported from the package:

```typescript
import type {
  SignalConfig,
  AskOptions, AskResponse,
  ChatOptions, ChatResponse,
  StreamChunk,
  PromptConfig, CreateVersionOptions,
  ResolveOptions, ResolvedPrompt,
  LogInteractionOptions, LogInteractionResponse,
  SetOutputOptions, ChatMessage, ToolCall,
  Feedback, SubmitFeedbackOptions,
} from '@genezio/signal';
```

## License

MIT