Skip to content

Commit 709a720

Browse files
fullsend-ai-coder[bot]gabemonteroclaude
authored
feat(#3310): add boost-toolscope and responses-api-toolkit packages (#3576)
* feat(#3310): add boost-toolscope and responses-api-toolkit packages Create two new standalone packages in the boost workspace: - @boost/toolscope (plugins/boost-toolscope): Provides a CacheAdapter interface for injectable caching, with InMemoryCacheAdapter for standalone use and BackstageCacheAdapter for bridging to Backstage coreServices.cache. Zero Backstage dependencies in the core interface — the BackstageCacheAdapter uses structural typing to remain compatible without direct imports. - @boost/responses-api-toolkit (plugins/boost-responses-api-toolkit): Extracts shared Responses API types, request building, and response parsing utilities from the llamastack provider module. Provides buildResponsesApiRequest for input translation, extractTextFromResponse for non-streaming parsing, and normalizeStreamEvent for SSE stream event normalization into boost NormalizedStreamEvent. Both packages include full test coverage (27 tests), API reports, ESLint configuration, and follow existing boost workspace conventions. Closes #3310 * fix: sync pluginPackages arrays for publish check Run 'backstage-cli repo fix --publish' to add pluginPackages to new packages and update existing packages to include the new entries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use strict undefined check for ttl in BackstageCacheAdapter Align with InMemoryCacheAdapter by using !== undefined instead of truthiness check, so ttl: 0 is not silently dropped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(boost): wire llamastack to use responses-api-toolkit - Replace 7 duplicate Responses API type definitions in llamastack types.ts with imports from boost-responses-api-toolkit - Refactor ResponsesApiProvider to delegate buildRequestBody, extractTextFromResponse, and normalizeStreamEvent to toolkit functions - Add 2 test cases to responseParser.test.ts achieving 100% branch coverage - Add task 5.3 to openspec for tracking the wiring change Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Co-authored-by: gabemontero <gmontero@redhat.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 381f36a commit 709a720

26 files changed

Lines changed: 1323 additions & 202 deletions

File tree

workspaces/boost/openspec/changes/pluggable-ai-platform-architecture/tasks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
- [ ] 5.1 Create `@boost/toolscope` package with injectable cache interface (`CacheAdapter`)
4949
- [ ] 5.2 Create `@boost/responses-api-toolkit` for shared Responses API utilities
50+
- [ ] 5.3 Update llamastack module to import Responses API types and utilities from `@boost/responses-api-toolkit`, removing duplicate type definitions from module-local `types.ts`
5051

5152
## 6. Dynamic Plugin Packaging (P2)
5253

workspaces/boost/plugins/boost-backend-module-llamastack/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"dependencies": {
3434
"@backstage/backend-plugin-api": "^1.9.1",
3535
"@red-hat-developer-hub/backstage-plugin-boost-common": "workspace:^",
36-
"@red-hat-developer-hub/backstage-plugin-boost-node": "workspace:^"
36+
"@red-hat-developer-hub/backstage-plugin-boost-node": "workspace:^",
37+
"@red-hat-developer-hub/backstage-plugin-boost-responses-api-toolkit": "workspace:^"
3738
},
3839
"devDependencies": {
3940
"@backstage/cli": "^0.34.5"

workspaces/boost/plugins/boost-backend-module-llamastack/src/provider/ResponsesApiProvider.ts

Lines changed: 21 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ import type {
2121
NormalizedStreamEvent,
2222
ProviderDescriptor,
2323
} from '@red-hat-developer-hub/backstage-plugin-boost-common';
24-
import type {
25-
LlamaStackConnectionConfig,
26-
ResponsesApiInputItem,
27-
ResponsesApiRequest,
28-
ResponsesApiResponse,
29-
ResponsesApiStreamEvent,
30-
} from '../types';
24+
import {
25+
buildResponsesApiRequest,
26+
extractTextFromResponse,
27+
normalizeStreamEvent,
28+
type ResponsesApiRequest,
29+
type ResponsesApiResponse,
30+
type ResponsesApiStreamEvent,
31+
} from '@red-hat-developer-hub/backstage-plugin-boost-responses-api-toolkit';
32+
import type { LlamaStackConnectionConfig } from '../types';
3133

3234
/**
3335
* Options for creating a {@link ResponsesApiProvider}.
@@ -101,7 +103,7 @@ export class ResponsesApiProvider implements AgenticProvider {
101103
}
102104

103105
const result = (await response.json()) as ResponsesApiResponse;
104-
return this.extractTextFromResponse(result);
106+
return extractTextFromResponse(result);
105107
}
106108

107109
/**
@@ -183,44 +185,19 @@ export class ResponsesApiProvider implements AgenticProvider {
183185
messages: InputItem[],
184186
stream: boolean,
185187
): ResponsesApiRequest {
186-
const skipped = messages.filter(m => m.type !== 'text');
187-
if (skipped.length > 0) {
188-
this.logger.debug(
189-
`Skipping ${skipped.length} non-text input item(s) (types: ${[...new Set(skipped.map(m => m.type))].join(', ')})`,
190-
);
191-
}
192-
193-
const input: ResponsesApiInputItem[] = messages
194-
.filter(
195-
(m): m is Extract<InputItem, { type: 'text' }> => m.type === 'text',
196-
)
197-
.map(m => ({
198-
role: 'user' as const,
199-
content: m.text,
200-
}));
201-
202-
return {
188+
const { body, skippedCount, skippedTypes } = buildResponsesApiRequest({
203189
model: this.connection.defaultModel ?? 'meta-llama/Llama-3.1-8B-Instruct',
204-
input,
190+
messages,
205191
stream,
206-
};
207-
}
192+
});
208193

209-
/**
210-
* Extract plain text from a non-streaming Responses API response.
211-
*/
212-
private extractTextFromResponse(result: ResponsesApiResponse): string {
213-
const parts: string[] = [];
214-
for (const output of result.output ?? []) {
215-
if (output.type === 'message' && output.content) {
216-
for (const part of output.content) {
217-
if (part.type === 'output_text') {
218-
parts.push(part.text);
219-
}
220-
}
221-
}
194+
if (skippedCount > 0) {
195+
this.logger.debug(
196+
`Skipping ${skippedCount} non-text input item(s) (types: ${skippedTypes.join(', ')})`,
197+
);
222198
}
223-
return parts.join('');
199+
200+
return body;
224201
}
225202

226203
/**
@@ -256,7 +233,7 @@ export class ResponsesApiProvider implements AgenticProvider {
256233

257234
try {
258235
const event = JSON.parse(data) as ResponsesApiStreamEvent;
259-
for (const normalized of this.normalizeStreamEvent(event)) {
236+
for (const normalized of normalizeStreamEvent(event)) {
260237
yield normalized;
261238
if (normalized.type === 'done') {
262239
return;
@@ -280,7 +257,7 @@ export class ResponsesApiProvider implements AgenticProvider {
280257
}
281258
try {
282259
const event = JSON.parse(data) as ResponsesApiStreamEvent;
283-
for (const normalized of this.normalizeStreamEvent(event)) {
260+
for (const normalized of normalizeStreamEvent(event)) {
284261
yield normalized;
285262
if (normalized.type === 'done') {
286263
return;
@@ -297,50 +274,4 @@ export class ResponsesApiProvider implements AgenticProvider {
297274
reader.releaseLock();
298275
}
299276
}
300-
301-
/**
302-
* Normalize a Responses API stream event into boost NormalizedStreamEvents.
303-
*/
304-
private *normalizeStreamEvent(
305-
event: ResponsesApiStreamEvent,
306-
): Iterable<NormalizedStreamEvent> {
307-
switch (event.type) {
308-
case 'response.output_text.delta':
309-
if (event.delta) {
310-
yield { type: 'text', text: event.delta };
311-
}
312-
break;
313-
314-
case 'response.mcp_call.in_progress':
315-
if (event.item?.id && event.item?.server_label) {
316-
yield {
317-
type: 'tool_call',
318-
toolCallId: event.item.id,
319-
toolName: event.item.server_label,
320-
args: '{}',
321-
};
322-
}
323-
break;
324-
325-
case 'response.mcp_call.completed':
326-
if (event.item?.id) {
327-
const resultText =
328-
event.item.content?.map(p => p.text).join('') ?? '';
329-
yield {
330-
type: 'tool_result',
331-
toolCallId: event.item.id,
332-
content: resultText,
333-
};
334-
}
335-
break;
336-
337-
case 'response.completed':
338-
yield { type: 'done' };
339-
break;
340-
341-
default:
342-
this.logger.debug(`Unhandled Responses API event type: ${event.type}`);
343-
break;
344-
}
345-
}
346277
}

workspaces/boost/plugins/boost-backend-module-llamastack/src/types.ts

Lines changed: 0 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -44,114 +44,6 @@ export interface LlamaStackModel {
4444
modelType?: string;
4545
}
4646

47-
/**
48-
* Llama Stack Responses API request body.
49-
*
50-
* @internal
51-
*/
52-
export interface ResponsesApiRequest {
53-
/** The model to use for this request. */
54-
model: string;
55-
/** The input messages for the response. */
56-
input: ResponsesApiInputItem[];
57-
/** Whether to stream the response. */
58-
stream?: boolean;
59-
/** Optional temperature for sampling. */
60-
temperature?: number;
61-
/** Optional max output tokens. */
62-
max_output_tokens?: number;
63-
/** Optional tool definitions. */
64-
tools?: ResponsesApiTool[];
65-
/** Optional previous response ID for multi-turn conversations. */
66-
previous_response_id?: string;
67-
}
68-
69-
/**
70-
* Input item for the Responses API.
71-
*
72-
* @internal
73-
*/
74-
export interface ResponsesApiInputItem {
75-
/** Role of the message author. */
76-
role: 'user' | 'assistant' | 'system';
77-
/** Message content. */
78-
content: string;
79-
}
80-
81-
/**
82-
* Tool definition for the Responses API.
83-
*
84-
* @internal
85-
*/
86-
export interface ResponsesApiTool {
87-
/** Tool type (e.g., 'mcp'). */
88-
type: string;
89-
/** Tool-specific server label. */
90-
server_label?: string;
91-
/** Tool-specific server URL. */
92-
server_url?: string;
93-
/** Optional headers for the tool server. */
94-
headers?: Record<string, string>;
95-
}
96-
97-
/**
98-
* Non-streaming response from the Responses API.
99-
*
100-
* @internal
101-
*/
102-
export interface ResponsesApiResponse {
103-
/** Response identifier. */
104-
id: string;
105-
/** The output items. */
106-
output: ResponsesApiOutputItem[];
107-
/** The model used. */
108-
model: string;
109-
}
110-
111-
/**
112-
* An output item in a Responses API response.
113-
*
114-
* @internal
115-
*/
116-
export interface ResponsesApiOutputItem {
117-
/** Output type. */
118-
type: 'message' | 'mcp_call' | 'mcp_list_tools';
119-
/** Message content (for message type). */
120-
content?: ResponsesApiContentPart[];
121-
/** Tool call ID (for mcp_call type). */
122-
id?: string;
123-
/** Server label (for mcp_call type). */
124-
server_label?: string;
125-
}
126-
127-
/**
128-
* A content part in a Responses API output message.
129-
*
130-
* @internal
131-
*/
132-
export interface ResponsesApiContentPart {
133-
/** Content type. */
134-
type: 'output_text';
135-
/** Text content. */
136-
text: string;
137-
}
138-
139-
/**
140-
* A streaming event from the Responses API.
141-
*
142-
* @internal
143-
*/
144-
export interface ResponsesApiStreamEvent {
145-
/** Event type discriminator. */
146-
type: string;
147-
/** Text delta for output_text_delta events. */
148-
delta?: string;
149-
/** Item snapshot for output_added events. */
150-
item?: ResponsesApiOutputItem;
151-
/** Response object for response.completed events. */
152-
response?: ResponsesApiResponse;
153-
}
154-
15547
/**
15648
* MCP server configuration for the Llama Stack provider.
15749
*

workspaces/boost/plugins/boost-backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"pluginPackages": [
1616
"@red-hat-developer-hub/backstage-plugin-boost-backend",
1717
"@red-hat-developer-hub/backstage-plugin-boost-common",
18-
"@red-hat-developer-hub/backstage-plugin-boost-node"
18+
"@red-hat-developer-hub/backstage-plugin-boost-node",
19+
"@red-hat-developer-hub/backstage-plugin-boost-responses-api-toolkit",
20+
"@red-hat-developer-hub/backstage-plugin-boost-toolscope"
1921
]
2022
},
2123
"exports": {

workspaces/boost/plugins/boost-common/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"pluginPackages": [
1919
"@red-hat-developer-hub/backstage-plugin-boost-backend",
2020
"@red-hat-developer-hub/backstage-plugin-boost-common",
21-
"@red-hat-developer-hub/backstage-plugin-boost-node"
21+
"@red-hat-developer-hub/backstage-plugin-boost-node",
22+
"@red-hat-developer-hub/backstage-plugin-boost-responses-api-toolkit",
23+
"@red-hat-developer-hub/backstage-plugin-boost-toolscope"
2224
]
2325
},
2426
"dependencies": {

workspaces/boost/plugins/boost-node/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"pluginPackages": [
1919
"@red-hat-developer-hub/backstage-plugin-boost-backend",
2020
"@red-hat-developer-hub/backstage-plugin-boost-common",
21-
"@red-hat-developer-hub/backstage-plugin-boost-node"
21+
"@red-hat-developer-hub/backstage-plugin-boost-node",
22+
"@red-hat-developer-hub/backstage-plugin-boost-responses-api-toolkit",
23+
"@red-hat-developer-hub/backstage-plugin-boost-toolscope"
2224
]
2325
},
2426
"dependencies": {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@red-hat-developer-hub/backstage-plugin-boost-responses-api-toolkit",
3+
"version": "0.1.0",
4+
"license": "Apache-2.0",
5+
"description": "Shared Responses API utilities for boost AI platform provider modules",
6+
"main": "src/index.ts",
7+
"types": "src/index.ts",
8+
"publishConfig": {
9+
"access": "public",
10+
"main": "dist/index.cjs.js",
11+
"module": "dist/index.esm.js",
12+
"types": "dist/index.d.ts"
13+
},
14+
"backstage": {
15+
"role": "node-library",
16+
"pluginId": "boost",
17+
"pluginPackage": "@red-hat-developer-hub/backstage-plugin-boost-responses-api-toolkit",
18+
"pluginPackages": [
19+
"@red-hat-developer-hub/backstage-plugin-boost-backend",
20+
"@red-hat-developer-hub/backstage-plugin-boost-common",
21+
"@red-hat-developer-hub/backstage-plugin-boost-node",
22+
"@red-hat-developer-hub/backstage-plugin-boost-responses-api-toolkit",
23+
"@red-hat-developer-hub/backstage-plugin-boost-toolscope"
24+
]
25+
},
26+
"dependencies": {
27+
"@red-hat-developer-hub/backstage-plugin-boost-common": "workspace:^"
28+
},
29+
"sideEffects": false,
30+
"scripts": {
31+
"build": "backstage-cli package build",
32+
"lint": "backstage-cli package lint",
33+
"test": "backstage-cli package test --passWithNoTests --coverage",
34+
"clean": "backstage-cli package clean",
35+
"prepack": "backstage-cli package prepack",
36+
"postpack": "backstage-cli package postpack"
37+
},
38+
"devDependencies": {
39+
"@backstage/cli": "^0.34.5"
40+
},
41+
"files": [
42+
"dist"
43+
],
44+
"repository": {
45+
"type": "git",
46+
"url": "git+https://github.com/redhat-developer/rhdh-plugins.git",
47+
"directory": "workspaces/boost/plugins/boost-responses-api-toolkit"
48+
},
49+
"homepage": "https://red.ht/rhdh",
50+
"bugs": "https://github.com/redhat-developer/rhdh-plugins/issues"
51+
}

0 commit comments

Comments
 (0)