Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6dcca3b
feat: Support orchestration prompt module fallback for non-streaming …
davidkna-sap Jan 23, 2026
fb1b980
fixes
davidkna-sap Jan 23, 2026
9658e43
chore: Fix pnpm typecheck script for pnpm v10.28.1 (#1460)
davidkna-sap Jan 23, 2026
27cd0df
feat: Support orchestration prompt module fallback
davidkna-sap Jan 26, 2026
6277d9c
Merge branch 'main' into davidkna-sap_o-mod-fb
KavithaSiva Feb 3, 2026
792a3e2
Merge branch 'main' into davidkna-sap_o-mod-fb
davidkna-sap Feb 3, 2026
7c1f969
address review comments
davidkna-sap Feb 3, 2026
ea56a0d
Merge branch 'davidkna-sap_o-mod-fb' into davidkna-sap_o-mod-fb-strea…
davidkna-sap Feb 3, 2026
1629a9b
use more specific error kinds
davidkna-sap Feb 3, 2026
58fa570
chore: fix timeout param
davidkna-sap Feb 3, 2026
156e10a
Merge branch 'main' into davidkna-sap_o-mod-fb
KavithaSiva Feb 4, 2026
42cfb60
chore: remove 4o-mini
davidkna-sap Feb 4, 2026
8a0c28b
Update sample-code/src/orchestration.ts
davidkna-sap Feb 4, 2026
cadfffa
Update packages/orchestration/src/orchestration-client.ts
davidkna-sap Feb 4, 2026
a3a1be1
Apply suggestions from code review
davidkna-sap Feb 4, 2026
77f904c
Merge branch 'main' into davidkna-sap_o-mod-fb
KavithaSiva Feb 6, 2026
42bef03
fix issues due to merge conflict resolution
davidkna-sap Feb 6, 2026
dd3ee02
Merge branch 'main' into davidkna-sap_o-mod-fb
KavithaSiva Feb 9, 2026
57f9392
Merge branch 'main' into davidkna-sap_o-mod-fb
KavithaSiva Feb 13, 2026
7066cb4
Update packages/orchestration/src/orchestration-client.ts
davidkna-sap Feb 13, 2026
6586e49
fix: Changes from lint
Feb 13, 2026
73591d5
Merge branch 'davidkna-sap_o-mod-fb' into davidkna-sap_o-mod-fb-strea…
davidkna-sap Feb 13, 2026
c6bdb2c
address review comments
davidkna-sap Feb 13, 2026
8941700
add changeset for streaming
davidkna-sap Feb 16, 2026
fd60597
Merge remote-tracking branch 'origin/main' into davidkna-sap_o-mod-fb…
davidkna-sap Feb 16, 2026
ac9c4c5
Update packages/orchestration/src/util/stream.ts
davidkna-sap Feb 16, 2026
6e05210
Enhance output filtering warnings for array stream options
davidkna-sap Feb 16, 2026
104907c
handle multiple stream options via overrides
davidkna-sap Feb 17, 2026
f106e9c
fix: Changes from lint
Feb 17, 2026
34eab1a
minor cleanup
davidkna-sap Feb 17, 2026
97b9b5b
Merge branch 'main' into davidkna-sap_o-mod-fb-streaming
KavithaSiva Feb 18, 2026
a909126
address review feedback
davidkna-sap Feb 18, 2026
f22b1b8
Merge branch 'main' into davidkna-sap_o-mod-fb-streaming
KavithaSiva Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curvy-mice-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ai-sdk/orchestration': minor
---

[feat] Support streaming with orchestration prompt module fallback.
3 changes: 3 additions & 0 deletions packages/orchestration/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export type {
ChatCompletionRequest,
RequestOptions,
StreamOptions,
StreamOptionsWithOverrides,
BaseStreamOptions,
ModuleStreamOptions,
DocumentGroundingServiceConfig,
DocumentGroundingServiceFilter,
DpiMaskingConfig,
Expand Down
356 changes: 355 additions & 1 deletion packages/orchestration/src/orchestration-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import type { CompletionPostResponse } from './client/api/schema/index.js';
import type {
OrchestrationModuleConfig,
OrchestrationConfigRef,
ChatCompletionRequest
ChatCompletionRequest,
StreamOptions,
StreamOptionsWithOverrides
} from './orchestration-types.js';

const defaultJsonConfig = `{
Expand Down Expand Up @@ -1318,4 +1320,356 @@ describe('orchestration service client', () => {
spy.mockRestore();
});
});

describe('Orchestration Client with module fallback configs', () => {
it('calls chatCompletion with module fallback configuration array', async () => {
const primaryConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-4o', timeout: 5, params: { max_tokens: 100 } },
prompt: {
template: [{ role: 'user', content: 'Try primary model' }]
}
}
};

const fallbackConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-5-mini', params: { max_tokens: 50 } },
prompt: {
template: [{ role: 'user', content: 'Fallback model' }]
}
}
};

const mockResponse = await parseMockResponse<CompletionPostResponse>(
'orchestration',
'orchestration-chat-completion-success-response.json'
);

mockInference(
{
data: constructCompletionPostRequest([primaryConfig, fallbackConfig])
},
{
data: mockResponse,
status: 200
},
{
url: 'inference/deployments/1234/v2/completion'
}
);

const response = await new OrchestrationClient([
primaryConfig,
fallbackConfig
]).chatCompletion();

expect(response).toBeInstanceOf(OrchestrationResponse);
expect(response.getContent()).toBe('Hello! How can I assist you today?');
});

it('calls streaming with module fallback configuration array', async () => {
const primaryConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-4o' },
prompt: {
template: [{ role: 'user', content: 'Primary model' }]
}
}
};

const fallbackConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-5-mini' },
prompt: {
template: [{ role: 'user', content: 'Fallback model' }]
}
}
};

mockInference(
{
data: constructCompletionPostRequest(
[primaryConfig, fallbackConfig],
undefined,
true
)
},
{
data: streamMockResponse,
status: 200
},
{
url: 'inference/deployments/1234/v2/completion'
}
);

const response = await new OrchestrationClient([
primaryConfig,
fallbackConfig
]).stream();

const chunks: string[] = [];
for await (const chunk of response.stream) {
const content = chunk.getDeltaContent();
if (content) {
chunks.push(content);
}
}

// Verify that streaming completed successfully and returned content
expect(chunks.length).toBeGreaterThan(0);
expect(chunks.join('')).toContain('SAP Cloud SDK');
});

it('returns intermediate failures from fallback attempts', async () => {
const primaryConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'non-existing-model' },
prompt: {
template: [{ role: 'user', content: 'Test' }]
}
}
};

const fallbackConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-5-mini' },
prompt: {
template: [{ role: 'user', content: 'Test' }]
}
}
};

const mockResponseWithErrors = {
...(await parseMockResponse<CompletionPostResponse>(
'orchestration',
'orchestration-chat-completion-success-response.json'
)),
intermediate_failures: [
{
message: 'Model non-existing-model not found',
code: 'MODEL_NOT_FOUND',
location: 'config[0]'
}
]
};

mockInference(
{
data: constructCompletionPostRequest([primaryConfig, fallbackConfig])
},
{
data: mockResponseWithErrors,
status: 200
},
{
url: 'inference/deployments/1234/v2/completion'
}
);

const response = await new OrchestrationClient([
primaryConfig,
fallbackConfig
]).chatCompletion();

const failures = response.getIntermediateFailures();
expect(failures).toBeDefined();
expect(failures).toHaveLength(1);
expect(failures![0].message).toBe('Model non-existing-model not found');
});

it('should throw when provided empty config array', () => {
expect(() => new OrchestrationClient([])).toThrow(
'Configuration array must not be empty.'
);
});
});

describe('Orchestration Client with per-config stream options', () => {
it('calls streaming with stream options for module fallback configs', async () => {
const primaryConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-4o' },
prompt: {
template: [{ role: 'user', content: 'Primary' }]
}
}
};

const fallbackConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-5-mini' },
prompt: {
template: [{ role: 'user', content: 'Fallback' }]
}
}
};

const streamOptions: StreamOptionsWithOverrides = {
promptTemplating: { include_usage: false },
overrides: {
0: { promptTemplating: { include_usage: true } }
}
};

mockInference(
{
data: constructCompletionPostRequest(
[primaryConfig, fallbackConfig],
undefined,
true,
streamOptions
)
},
{
data: streamMockResponse,
status: 200
},
{
url: 'inference/deployments/1234/v2/completion'
}
);

const response = await new OrchestrationClient([
primaryConfig,
fallbackConfig
]).stream({}, undefined, streamOptions);

const chunks: string[] = [];
for await (const chunk of response.stream) {
const content = chunk.getDeltaContent();
if (content) {
chunks.push(content);
}
}

expect(chunks.length).toBeGreaterThan(0);
expect(chunks.join('')).toContain('SAP Cloud SDK');
});

it('calls streaming with array-based overrides', async () => {
const primaryConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-4o' },
prompt: {
template: [{ role: 'user', content: 'Primary' }]
}
}
};

const fallbackConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-5-mini' },
prompt: {
template: [{ role: 'user', content: 'Fallback' }]
}
}
};

const streamOptions: StreamOptions = {
global: { chunk_size: 100 },
overrides: [
{ promptTemplating: { include_usage: true } },
{ promptTemplating: { include_usage: false } }
] as any
};

mockInference(
{
data: constructCompletionPostRequest(
[primaryConfig, fallbackConfig],
undefined,
true,
streamOptions
)
},
{
data: streamMockResponse,
status: 200
},
{
url: 'inference/deployments/1234/v2/completion'
}
);

const response = await new OrchestrationClient([
primaryConfig,
fallbackConfig
]).stream({}, undefined, streamOptions);

const chunks: string[] = [];
for await (const chunk of response.stream) {
const content = chunk.getDeltaContent();
if (content) {
chunks.push(content);
}
}

expect(chunks.length).toBeGreaterThan(0);
expect(chunks.join('')).toContain('SAP Cloud SDK');
});

it('warns when override index is out of bounds', async () => {
const primaryConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-4o' },
prompt: {
template: [{ role: 'user', content: 'Primary' }]
}
}
};

const fallbackConfig: OrchestrationModuleConfig = {
promptTemplating: {
model: { name: 'gpt-5-mini' },
prompt: {
template: [{ role: 'user', content: 'Fallback' }]
}
}
};

const streamOptions: StreamOptionsWithOverrides = {
overrides: {
0: { promptTemplating: { include_usage: true } },
2: { promptTemplating: { include_usage: false } }
}
};

const logger = createLogger({
package: 'orchestration',
messageContext: 'orchestration-utils'
});
const debugSpy = jest.spyOn(logger, 'debug');

mockInference(
{
data: constructCompletionPostRequest(
[primaryConfig, fallbackConfig],
undefined,
true,
streamOptions
)
},
{
data: streamMockResponse,
status: 200
},
{
url: 'inference/deployments/1234/v2/completion'
}
);

await new OrchestrationClient([primaryConfig, fallbackConfig]).stream(
{},
undefined,
streamOptions
);

expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('2'));
expect(debugSpy).toHaveBeenCalledWith(
expect.stringContaining('do not correspond to any module configuration')
);
});
});
});
Loading
Loading