Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
164806 stories
·
33 followers

Node.js 26.13.1 (LTS)

1 Share
Read the whole story
alvinashcraft
31 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Node.js 24.22.3 (LTS)

1 Share
Read the whole story
alvinashcraft
36 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Node.js 22.23.5 (LTS)

1 Share
Read the whole story
alvinashcraft
40 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Migrate a GitHub repository to GitHub Enterprise

1 Share

This posts shows how to migrate a repository from GitHub.com to GitHub Enterprise Cloud (GHE.com). The migration transfers the repository and its associated data to the GitHub Enterprise environment while preserving project continuity and maintaining access for authorized users.

Step 1: Install required tools for the GitHub migration.

Install the required tools for the GitHub migration in advance, including PowerShell and the GitHub CLI (gh). Ensure both tools are installed and available from the command line.

winget install --id GitHub.cli --source winget

gh extension install github/gh-gei

https://cli.github.com

Step 2: Create the GitHub.com, GHE.com Access Tokens

Create a Personal Access Token (Classic) on GitHub.com/ghe.com for both the source and destination. Ensure that you select Personal access tokens (classic) and not a fine-grained personal access token, as the migration process requires a classic PAT. When creating the token, grant the required permissions as specified for the migration and store the token securely, as it will only be displayed once. (Personal access tokens (classic))

Source:

https://github.com/settings/tokens

Select the required source scopes:

  • repo
  • admin:org

Destination:

Do the same for the destination GitHub Enterprise:

https://{your-enterprise}.ghe.com/settings/tokens

Required scopes for the destination:

  • repo
  • workflow
  • admin:org

Step 3: Configure SSO

Ensure that SSO is configured and authorized for both Personal Access Tokens (PATs) in the GitHub UI. Select the correct SSO for the target and destinations GitHub enterprises.

Step 4: Migrate the repository

I use Powershell for this. Add the two tokens:

$env:GH_PAT="TOKEN"
$env:GH_SOURCE_PAT="TOKEN"

Run the migration using the correct parameters for your setup.

gh gei migrate-repo --github-source-org SOURCE 
     --source-repo CURRENT-NAME 
     --github-target-org DESTINATION 
     --target-repo NEW-NAME 
     --target-api-url "TARGET-API-URL"

Step 5: Validate the new repository and delete the tokens

Validate that the repository was correctly migrated and delete the two PAT from GitHub and GHE.

Notes, troubleshooting

If you get this error the following error, configure the SSO for your PAT.

[2026-11-09 13:10:54] [ERROR] Resource protected by organization SAML enforcement. 
You must grant your Personal Access token access to this organization.

PR Error: Adjust the GHE settings.

Error during migration: Git source migration failed. Error message: Push error: Errors reported by remote:
error: GH013: Repository rule violations found for refs/heads/main.
error: GH013: Repository rule violations found for refs/github-services/pull/1/head.
References rejected by remote:
main => push declined due to repository rule violations
refs/github-services/pull/1/head => push declined due to repository rule violations

FIX:

Open the Enterprise and reset the repository settings.

https://{your-ghe}.ghe.com/enterprises/{–name–}

enterprises/{your-ghe}/settings/policies/repositories

Disable all the custom defined rulesets and do the migration. When complete re-enable the rulesets.

Already exists:

[WARNING] The Org ‘{org-name}’ already contains a repository with the name ‘RepoName’. No operation will be performed

Delete the repository in the destination GHE organisation.

Links

https://docs.github.com/en/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-githubcom-to-github-enterprise-cloud

https://docs.github.com/en/migrations/using-ghe-migrator

https://github.com/cli/cli/blob/trunk/docs/install_windows.md#winget



Read the whole story
alvinashcraft
55 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Stop Calling getGenerativeModel() Everywhere! The Hidden Traps of Scaling Firebase AI to Multi-Model Angular Apps

1 Share

Calling getGenerativeModel() directly inside VisionService and TextToSpeechService duplicates firebase/ai imports, safety settings, and thinking configurations while recreating client-side GenerativeModel objects on every request and triggering unnecessary garbage collection. In this post, we extract AiModelFactory to centralize and cache model instances for multi-model routing, and we resolve two hidden caching traps: request-level generationConfig overwrites and stale AI backends after Remote Config updates.

Architecture & Mental Model

VisionService and TextToSpeechService delegate model creation and lifecycle management to AiModelFactory:

  • On-Demand Model Requests: VisionService calls getModel(options) with AiModelOptions, while TextToSpeechService calls getTtsModel().
  • Backend Validation and Cache Eviction (getValidBackend()): Before returning a model, getValidBackend() compares ConfigService.getAiBackend() against #cachedBackend. If Remote Config refreshes the AI backend, it clears #models and resets #ttsModel to null.
  • Keyed Vision Model Cache (#models): getModel(options) builds a deterministic key via generateKey() and caches instances in #models (Map<string, GenerativeModel>).
  • Singleton TTS Model Cache (#ttsModel): getTtsModel() caches a single #ttsModel instance configured with responseModalities: [ResponseModality.AUDIO].
flowchart TD
    subgraph Callers["Domain Services"]
        VS["VisionService (1 model now -> 3 models with MediaPipe routing)"]
        TTS["TextToSpeechService (per-request voice speechConfig)"]
    end

    subgraph Factory["AiModelFactory (@/core/firebase)"]
        GVB["getValidBackend(): compare with #cachedBackend"]
        EVICT["Backend changed? #models.clear() & #ttsModel = null"]
        GM["getModel(options?: AiModelOptions)"]
        KEY["generateKey(): model_thinking_instruction_schema_tools"]
        MAP[("#models: Map<string, GenerativeModel>")]
        GTTS["getTtsModel()"]
        TTS_CACHE[("#ttsModel: GenerativeModel | null")]
    end

    VS -- "getModel(options)" --> GVB
    TTS -- "getTtsModel()" --> GVB
    GVB -- "New AI instance" --> EVICT --> GM & GTTS
    GVB -- "Same AI instance" --> GM & GTTS
    GM --> KEY --> MAP
    GTTS --> TTS_CACHE

1. Defining AiModelOptions and Deterministic Cache Keys

1.1. The AiModelOptions Interface

The AiModelOptions interface defines the configuration needed to create a vision model and compute its cache key:

  • Optional Configuration Properties: systemInstruction, schema (SchemaRequest | TypedSchema), and the tools array (Tool[]) let callers configure structured JSON output, plain-text responses, or grounding tools.
// src/app/core/firebase/ai-model-options.interface.ts
import type { SchemaRequest, Tool, TypedSchema } from 'firebase/ai';

export interface AiModelOptions {
  systemInstruction?: string;
  schema?: SchemaRequest | TypedSchema;
  tools?: Tool[];
}

1.2. Private State and generateKey() in AiModelFactory

  • Configuration Injection (#configService): AiModelFactory injects ConfigService into #configService to read Remote Config values and obtain the Firebase AI Logic backend.
  • Cached AI Backend (#cachedBackend): #cachedBackend stores the active AI backend reference used to create generative models.
  • Singleton TTS Cache (#ttsModel): #ttsModel holds the single-instance TTS model used to synthesize speech from English text.
  • Vision Model Map (#models and generateKey): #models is a Map<string, GenerativeModel> that stores vision models by the key returned from generateKey(). The key joins the model name (geminiModelName), thinkingLevel, systemInstruction, the structured-output schema, and tools with underscore (_) characters.
// src/app/core/firebase/ai-model-factory.service.ts (fields & key generation)
import type { AI, GenerationConfig, GenerativeModel, ModelParams, SafetySetting } from 'firebase/ai';

@Service()
export class AiModelFactory {
  readonly #configService = inject(ConfigService);
  readonly #models = new Map<string, GenerativeModel>();
  #ttsModel: GenerativeModel | null = null;
  #cachedBackend: AI | null = null;

  private generateKey(options?: AiModelOptions): string {
    const { geminiModelName, thinkingLevel } = this.#configService.appConfig;
    const instruction = options?.systemInstruction ?? '';
    const schemaKey = options?.schema ? JSON.stringify(options.schema) : '';
    const toolsKey = options?.tools?.length ? JSON.stringify(options.tools) : '';

    return `${geminiModelName}_${thinkingLevel}_${instruction}_${schemaKey}_${toolsKey}`;
  }

2. Creating Models On Demand and Invalidating the Cache in AiModelFactory

2.1. Lazily Loading the SDK and Assembling Parameters (createModel, createModelParams, createGenerationConfig)

It is tempting to import the HarmBlockThreshold and HarmCategory enums at the top of the file, but doing so leaks firebase/ai runtime code into main.js and increases the initial bundle size:

  • Dynamic SDK Import (createModel): createModel() dynamically imports getGenerativeModel, HarmBlockThreshold, and HarmCategory from firebase/ai.
  • Shared Safety Settings (createModel): createModel() builds the SafetySetting[] array and calls getGenerativeModel(backend, modelParams).
  • Thinking Config (createGenerationConfig): createGenerationConfig() configures thinkingLevel with includeThoughts: true.
  • Structured Output Schema (createGenerationConfig): createGenerationConfig() attaches responseSchema and responseMimeType: 'application/json' when the caller provides options?.schema.
  • Model Parameter Assembly (createModelParams): createModelParams() combines the model name, generationConfig, and safetySettings, and conditionally adds tools and systemInstruction.
  // src/app/core/firebase/ai-model-factory.service.ts (model creation helpers)
  private async createModel(backend: AI, options?: AiModelOptions): Promise<GenerativeModel> {
    const { getGenerativeModel, HarmBlockThreshold, HarmCategory } = await import('firebase/ai');
    const safetySettings: SafetySetting[] = [
      { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
      { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
      { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
      { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH },
    ];

    const modelParams = this.createModelParams(safetySettings, options);
    return getGenerativeModel(backend, modelParams);
  }

  private createModelParams(safetySettings: SafetySetting[], options: AiModelOptions | undefined): ModelParams {
    const generationConfig = this.createGenerationConfig(options);

    const modelParams: ModelParams = {
      model: this.#configService.appConfig.geminiModelName,
      generationConfig,
      safetySettings,
    };

    if (options?.tools?.length) {
      modelParams.tools = options.tools;
    }

    if (options?.systemInstruction) {
      modelParams.systemInstruction = options.systemInstruction;
    }
    return modelParams;
  }

  private createGenerationConfig(options: AiModelOptions | undefined): GenerationConfig {
    const generationConfig: GenerationConfig = {
      thinkingConfig: {
        thinkingLevel: this.#configService.appConfig.thinkingLevel,
        includeThoughts: true,
      },
    };

    if (options?.schema) {
      generationConfig.responseSchema = options.schema;
      generationConfig.responseMimeType = 'application/json';
    }
    return generationConfig;
  }

2.2. Reusing Models On Demand (getModel and getTtsModel)

Obtaining a vision model and a TTS model follows two different caching flows in AiModelFactory:

  • Vision Model Lookup (getModel): getModel() generates a deterministic cache key from AiModelOptions via generateKey(options) and checks this.#models.get(key).
  • Vision Model Cache Miss: When the key is not in #models, getModel() calls createModel() to instantiate the model and stores it in the map.
  • TTS Model Flow (getTtsModel): getTtsModel() calls createTtsModel() once to instantiate a singleton TTS model configured with responseModalities: [ResponseModality.AUDIO] and assigns it to #ttsModel.
  • Eliminating Per-Call Allocation and Garbage Collection: Reusing cached instances across requests avoids repeated getGenerativeModel() calls and unnecessary garbage collection.
  // src/app/core/firebase/ai-model-factory.service.ts (on-demand getters)
  async getModel(options?: AiModelOptions): Promise<GenerativeModel> {
    const backend = await this.getValidBackend();
    const key = this.generateKey(options);
    let model = this.#models.get(key);
    if (!model) {
      model = await this.createModel(backend, options);
      this.#models.set(key, model);
    }

    return model;
  }

  private async createTtsModel(ai: AI): Promise<GenerativeModel> {
    const { getGenerativeModel, ResponseModality } = await import('firebase/ai');

    return getGenerativeModel(ai, {
      model: this.#configService.appConfig.geminiTTSModelName,
      generationConfig: {
        responseModalities: [ResponseModality.AUDIO],
      },
    });
  }

  async getTtsModel(): Promise<GenerativeModel> {
    const backend = await this.getValidBackend();
    if (!this.#ttsModel) {
      this.#ttsModel = await this.createTtsModel(backend);
    }

    return this.#ttsModel;
  }

2.3. Evicting Cached Models When Remote Config Rotates the Backend (getValidBackend)

  • Stale Backend on Remote Config Refresh: Constructing a GenerativeModel requires a valid Firebase AI Logic AI backend, which becomes outdated when dependent Firebase Remote Config parameters (such as useLimitedUseAppCheckTokens and vertexAILocation) receive updated values and ConfigService resets its AI instance.
  • Purging Cached Models (getValidBackend): When getValidBackend() detects that this.#configService.getAiBackend() no longer matches #cachedBackend, it updates #cachedBackend, clears the #models map, and resets #ttsModel to null.
  // src/app/core/firebase/ai-model-factory.service.ts (cache invalidation)
  private async getValidBackend(): Promise<AI> {
    const backend = await this.#configService.getAiBackend();
    if (backend !== this.#cachedBackend) {
      this.#cachedBackend = backend;
      this.#models.clear();
      this.#ttsModel = null;
    }

    return this.#cachedBackend;
  }
}

3. Refactoring VisionService and TextToSpeechService (and the Shallow-Spread Trap)

3.1. Simplifying VisionService for Future Multi-Model Routing

We refactored VisionService for extensibility and scalability by delegating model creation to AiModelFactory:

  • Removing SDK Setup Boilerplate: Introducing AiModelFactory removes SAFETY_SETTINGS, ConfigService, and the private getGenerativeAIModel() helper from VisionService.
  • Extracting altTextPrompt: Moving altTextPrompt to vision.prompt.ts keeps VisionService focused on orchestrating image analysis—generating tags, writing accessible alt text, and finding an obscure fact.
  • On-Demand Model Retrieval with Custom Tools: VisionService injects AiModelFactory into #aiModelFactory and calls this.#aiModelFactory.getModel({ schema: ImageAnalysisSchema, tools: [{ googleSearch: {} }] }). While the current model uses the Google Search grounding tool, VisionService can pass a Google Maps tool or custom function-calling tools through AiModelOptions to configure additional models.
  • Ready for MediaPipe Multi-Model Routing: Because getModel() caches instances by their configuration key, we can extend VisionService with an on-device MediaPipe classifier to request 3 specialized models (places, people and faces, and general objects) on demand.
// src/app/core/vision/vision.service.ts (refactored excerpt)
@Service()
export class VisionService {
  readonly #window = inject(WINDOW);
  readonly #aiModelFactory = inject(AiModelFactory);

  async analyzeImage(image: File): Promise<ImageAnalysisResponse> {
    const { data, mimeType, optimizationMetrics } = await preprocessImageForVision(image, this.#window);
    const imagePart: InlineDataPart = {
      type: 'inlineData',
      inlineData: { data, mimeType },
    };

    const aiModel = await this.#aiModelFactory.getModel({
      schema: ImageAnalysisSchema,
      tools: [{ googleSearch: {} }],
    });
    const result = await aiModel.generateContent([altTextPrompt, imagePart]);
    // ... parse response, tokenUsage, and grounding citations
  }
}

3.2. Reusing #ttsModel in TextToSpeechService Without Wiping Out responseModalities

Similarly, TextToSpeechService delegates TTS model creation to AiModelFactory while passing the selected voice per request:

  • Shared TTS Model Lookup: TextToSpeechService injects AiModelFactory into #aiModelFactory, and createAIModelGenerationRequest() retrieves the cached TTS model via this.#aiModelFactory.getTtsModel() instead of instantiating a new model for every synthesis call.
  • Per-Request speechConfig Assembly: The createAIModelGenerationRequest(text, voiceName) helper builds the GenerateContentRequest (shared by synthesizeStream() and synthesize()) and configures speechConfig with the requested voiceName and languageCode: 'en-US'.
  • Preserving ResponseModality.AUDIO Against Shallow Overwrites: In @firebase/ai, passing a request-level generationConfig overwrites the model's default aiModel.generationConfig. Spreading ...aiModel.generationConfig into the request's generationConfig preserves responseModalities: [ResponseModality.AUDIO] alongside speechConfig.
// src/app/core/speech/text-to-speech.service.ts (refactored excerpt)
@Service()
export class TextToSpeechService {
  readonly #aiModelFactory = inject(AiModelFactory);

  private async createAIModelGenerationRequest(text: string, voiceName: string): Promise<AIModelGenerativeRequest> {
    const aiModel = await this.#aiModelFactory.getTtsModel();
    const contents: Content[] = [
      {
        role: 'user',
        parts: [{ type: 'text', text }],
      },
    ];
    const generationConfig: GenerationConfig = {
      ...aiModel.generationConfig,
      speechConfig: {
        voiceConfig: {
          prebuiltVoiceConfig: { voiceName },
        },
        languageCode: 'en-US',
      },
    };

    return {
      aiModel,
      generationRequest: { contents, generationConfig },
    };
  }

  async *synthesizeStream({ text, voice }: SpeechPrompt): AsyncGenerator<AudioStreamChunk> {
    const { aiModel, generationRequest } = await this.createAIModelGenerationRequest(text, voice);
    const responseStream = await aiModel.generateContentStream(generationRequest);

    for await (const chunk of responseStream.stream) {
      const chunkData = this.extractValidChunkData(chunk);
      if (chunkData) {
        yield decodeAudioChunk(chunkData.data, chunkData.mimeType);
      }
    }
  }

  async synthesize({ text, voice }: SpeechPrompt): Promise<Blob> {
    const { aiModel, generationRequest } = await this.createAIModelGenerationRequest(text, voice);
    const result = await aiModel.generateContent(generationRequest);

    const chunk = this.extractValidChunkData(result.response);
    if (!chunk) {
      throw new Error('No audio data received in response.');
    }
    return toWavBlob(chunk.data, chunk.mimeType);
  }
}

Takeaways

  • Centralized Lazy Model Creation: AiModelFactory dynamically imports firebase/ai to construct both text-to-speech and image-analysis models. It centralizes safety settings and thinkingLevel for the image-analysis model while locking responseModalities to [ResponseModality.AUDIO] for the TTS model.
  • Instance Reuse Over Per-Request Allocation: AiModelFactory caches models in the #models map or the #ttsModel field and reuses them across requests, eliminating unnecessary per-request GenerativeModel instantiation and garbage collection.
  • Scalable Multi-Model Routing for VisionService: Keyed caching in the #models map makes VisionService scalable so an upcoming MediaPipe classifier can route images to specialized models (places, people and faces, and general objects) for analysis.
  • Preserving ResponseModality.AUDIO Across Per-Request Voices: Because text-to-speech synthesis accepts a voice per request, spreading ...aiModel.generationConfig into the request's generationConfig preserves ResponseModality.AUDIO alongside the selected voiceName.
  • Automatic Cache Invalidation on Remote Config Updates: When ConfigService fetches Remote Config values in the background and invalidates the cached AI backend, AiModelFactory purges all cached models and recreates fresh instances on the next call.

Resources

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

GraphQL And The Human Reasons Behind Our API Patterns

1 Share

Yesterday I shared a screenshot on LinkedIn, and it took off. More than 32,000 people have seen it, and the comments are still coming in. The screenshot was of a Bluesky post by Jan Lehnardt, quoting a thread by James Coglan. Here is the whole exchange:

James Coglan: been finally learning what graphql is due to <%= reasons %> and I have never seen such a clear case of a technical solution to a social problem. that problem being “too many people want to talk to the API devs”

Jan Lehnardt: Fun fact, I once sat down with the inventor of GraphQL and after 30 minutes of him trying to explain the technical merits he finally came out with it: “GraphQL exists so Facebook frontend devs don’t have to talk to Facebook backend devs”.

I didn’t share it to dunk on GraphQL. I shared it because it’s the cleanest example I’ve seen of something I’ve been saying for fifteen years. There is almost always a human reason for why we have different API patterns, forks in the road and glitches in the matrix. In my experience it’s rarely a technical reason. It’s almost always business or politics.

What the comments said

The comments are better than the post, and they’re worth reading in full. They clustered around a handful of ideas.

Conway’s Law. A lot of people went straight there: organizations ship systems shaped like their communication structures, and this was called the clearest example of it ever. Someone else called it quintessentially sociotechnical, and I agree.

Avoiding coordination isn’t a bad thing. Several people pushed back, and they had a point. Not having to coordinate is where agility and velocity come from. Documentation exists for the same reason: so a person doesn’t have to answer the same question a hundred times. One commenter asked a sharp question. Is the problem “frontend devs don’t want to talk to backend devs,” or “too many frontend devs want to talk to backend devs”? The first is a relationship problem, and technology is a bad fix for it. The second is a bottleneck, and automating away the communication overhead is a reasonable fix. Another commenter answered that it’s both, because even without a bottleneck, teams that don’t have to coordinate ship faster.

The split was the mistake. One of the best comments said the trouble is applying “don’t make us talk” horizontally. When you split teams into frontend and backend, neither one can ship anything a user values on its own, so they’re forced to talk about everything. The fix isn’t a query language. It’s teams organized around something a user actually gets.

Scale is the real use case. Others gave GraphQL its due. It makes sense when your API is so widely used that you can’t predict what clients will ask for, and the backend absorbs the cost and complexity of every possible permutation. That describes Facebook. People pointed to Shopify as a platform where building on top got easier with GraphQL. The problem, as one commenter put it, is how many startups with focused, predictable client use cases adopted a pattern designed for a scale they would never reach.

It doesn’t remove the conversation. My favorite irony came from an architect who said that in his experience GraphQL doesn’t have a well-defined enough contract, so the consumer ends up meeting with the producer anyway, to understand how queries behave. When it works, it’s great. When it doesn’t, you have meetings.

And the people who lived it. A former VP of engineering said he spent most of his days designing technical solutions to keep his team from warring with each other. Someone else described asking for a tiny change and being told it needed a new endpoint and would take three months. Another told a long story about a locker, a key-master and a Jira ticket, and ended with “I’d rather juggle 50 extra technical dependencies than a single Mitch.” Everyone has a Mitch.

What I take from it

When someone asked whether this isn’t just a perk of APIs in general, I said I think there’s a separation between APIs and the creation of APIs. An API is the interface. Creating one is the management of resources: who owns the data, who gets to change it, who has to be asked, how long it takes and who pays. That second part is where most of our patterns come from.

That’s true of GraphQL. It’s also true of most of the forks in the road I’ve watched over the years. SOAP and REST, Swagger and OpenAPI, the many ways to do webhooks or pagination or versioning. Each comes with a technical story, and the technical story is real. But underneath it is usually a team that didn’t want to wait on another team, a company that wanted control of a standard, a platform that needed its partners to move faster, or a budget that decided who did the work. The technology is how the human decision got written down.

That’s why I don’t think the useful question about GraphQL, or REST, or MCP, or A2A, is which one is better. It’s which human problem you actually have. If your problem is that you can’t predict what thousands of clients will need, GraphQL was built for you. If your problem is that two teams don’t get along, no query language will fix it, and you’ll end up in the meeting anyway. If your problem is that one team is a bottleneck for everyone else, a well-described contract, good documentation and a self-service way in will do more than anything else.

It’s also why I spend so much time on governance, contracts and the business of APIs, not just the technology. The contract is the agreement between the people on either side of the interface. The pattern you choose tells you a lot about the agreement those people were able, or unable, to make.

Thanks to everyone who jumped into the thread. Fifteen years in, the API conversations I learn the most from are still the ones about people.



Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories